From 3b147051671fc3f4a1ce2ba03bdedb900dc0f851 Mon Sep 17 00:00:00 2001 From: Jecket1 <134037206+Jecket1@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:09:39 +0800 Subject: [PATCH] Initnftlistamount (#180) (#182) * add SetCollectionListTotalCount * remove role check * feat: Revise the authorization count verification logic to compare the total number of listings with the authorization count on a per-collection basis. * feat: remove listing when allowance is not enough --- contract/Forest/ForestContract.cs | 8 ++ contract/Forest/ForestContract_Buyers.cs | 26 ++-- contract/Forest/ForestContract_Helpers.cs | 11 +- contract/Forest/ForestContract_Sellers.cs | 2 +- contract/Forest/ForestContract_Views.cs | 6 +- contract/Forest/Services/DealService.cs | 81 +++++++---- protobuf/forest_contract.proto | 9 ++ test/Forest.Tests/ForestContractTest_Init.cs | 2 +- ...restContractTests_MakeOffer_BatchBuyNow.cs | 133 +++++++++++++++++- 9 files changed, 235 insertions(+), 43 deletions(-) diff --git a/contract/Forest/ForestContract.cs b/contract/Forest/ForestContract.cs index 4ce475de..c6e92430 100755 --- a/contract/Forest/ForestContract.cs +++ b/contract/Forest/ForestContract.cs @@ -257,5 +257,13 @@ public override Empty SetMaxBatchCancelListCount(Int32Value input) State.MaxBatchCancelListCount.Value = input.Value; return new Empty(); } + + public override Empty SetCollectionListTotalCount(SetCollectionListTotalCountInput input) + { + Assert(input != null && input.Address != null && !string.IsNullOrEmpty(input.Symbol) && input.Count >=0, "Invalid input."); + + State.ListedNFTTotalAmountMap[input.Symbol][input.Address] = input.Count.ToString(); + return new Empty(); + } } } \ No newline at end of file diff --git a/contract/Forest/ForestContract_Buyers.cs b/contract/Forest/ForestContract_Buyers.cs index 9a3ab4ab..001a606c 100644 --- a/contract/Forest/ForestContract_Buyers.cs +++ b/contract/Forest/ForestContract_Buyers.cs @@ -229,15 +229,25 @@ public override Empty BatchBuyNow(BatchBuyNowInput input) Assert(nftInfo != null && !string.IsNullOrWhiteSpace(nftInfo.Symbol), "Invalid symbol data"); var userBalanceDic = new Dictionary(); var failPriceDic = new Dictionary(); + var listOwnerAllowanceDic = new Dictionary(); + foreach (var fixPrice in input.FixPriceList) { - SingleMakeOfferForBatchBuyNow(input.Symbol, new FixPrice() + Assert(fixPrice.Quantity > 0, "Invalid param Quantity."); + Assert(fixPrice.Price.Amount > 0, "Invalid price amount."); + Assert(fixPrice.OfferTo != null, "Invalid param OfferTo."); + if(!listOwnerAllowanceDic.ContainsKey(fixPrice.OfferTo.ToBase58())) + { + listOwnerAllowanceDic[fixPrice.OfferTo.ToBase58()] =GetAllowance(fixPrice.OfferTo, input.Symbol); + } + + SingleMakeOfferForBatchBuyNow(input.Symbol, new FixPrice() { OfferTo = fixPrice.OfferTo, Quantity = fixPrice.Quantity, Price = fixPrice.Price, StartTime = fixPrice.StartTime - }, userBalanceDic, failPriceDic); + }, userBalanceDic, failPriceDic, listOwnerAllowanceDic); } Context.Fire(new BatchBuyNowResult @@ -255,11 +265,9 @@ public override Empty BatchBuyNow(BatchBuyNowInput input) private void SingleMakeOfferForBatchBuyNow(string symbol, FixPrice inputFixPrice , Dictionary userBalanceDic - , Dictionary failPriceDic) + , Dictionary failPriceDic + , Dictionary listOwnerAllowanceDic) { - Assert(inputFixPrice.Quantity > 0, "Invalid param Quantity."); - Assert(inputFixPrice.Price.Amount > 0, "Invalid price amount."); - Assert(inputFixPrice.OfferTo != null, "Invalid param OfferTo."); var nftInfo = State.TokenContract.GetTokenInfo.Call(new GetTokenInfoInput { Symbol = symbol, @@ -287,14 +295,14 @@ private void SingleMakeOfferForBatchBuyNow(string symbol, FixPrice inputFixPrice Assert(nftInfo.Supply > 0, "NFT does not exist."); var dealService = GetDealService(); + var toRemove = new ListedNFTInfoList(); + var normalPriceDealResultList = dealService.GetDealResultListForBatchBuy(symbol, inputFixPrice, new ListedNFTInfoList { Value = { affordableNftInfoList } - }, failPriceDic).ToList(); - Assert(normalPriceDealResultList.Count > 0, "NormalPrice does not exist."); + }, failPriceDic, toRemove, listOwnerAllowanceDic).ToList(); - var toRemove = new ListedNFTInfoList(); foreach (var dealResult in normalPriceDealResultList) { var listedNftInfo = affordableNftInfoList[dealResult.Index]; diff --git a/contract/Forest/ForestContract_Helpers.cs b/contract/Forest/ForestContract_Helpers.cs index c9716119..d4f31896 100644 --- a/contract/Forest/ForestContract_Helpers.cs +++ b/contract/Forest/ForestContract_Helpers.cs @@ -278,7 +278,7 @@ private void AssertAllowanceInsufficient(string symbol, Address address, long cu var totalAmount = amount.Add(currentAmount); Assert(allowance >= totalAmount, $"The allowance you set is less than required. Please reset it."); } - + private long GetOfferTotalAmount(Address address, string symbol) { Assert(address != null, $"Invalid param Address"); @@ -292,6 +292,14 @@ private long GetEffectiveListedNFTTotalAmount(Address address, string symbol) Assert(address != null, $"Invalid param Address"); Assert(symbol != null, $"Invalid param Symbol"); + var collectionSymbol = TransferCollectionSymbol(symbol); + var collectionListedNFTTotalAmount = State.ListedNFTTotalAmountMap[collectionSymbol][address]; + + if (collectionListedNFTTotalAmount != null && collectionListedNFTTotalAmount != "") + { + return long.Parse(collectionListedNFTTotalAmount); + } + var listedNftInfoList = State.ListedNFTInfoListMap[symbol][address]; var totalAmount = 0L; if (listedNftInfoList != null) @@ -305,6 +313,7 @@ private long GetEffectiveListedNFTTotalAmount(Address address, string symbol) } } } + return totalAmount; } diff --git a/contract/Forest/ForestContract_Sellers.cs b/contract/Forest/ForestContract_Sellers.cs index 3e5cd019..c5acd2eb 100644 --- a/contract/Forest/ForestContract_Sellers.cs +++ b/contract/Forest/ForestContract_Sellers.cs @@ -126,7 +126,7 @@ public override Empty ListWithFixedPrice(ListWithFixedPriceInput input) var collectionAllowance = State.ListedNFTTotalAmountMap[collectionSymbol][Context.Sender]; if (collectionAllowance == null || collectionAllowance == "") { - var listedNFTTotalAmount = (allowance >= MaxApproveAllowance) ? input.Quantity : Math.Max(allowance, input.Quantity); + var listedNFTTotalAmount = input.Quantity; State.ListedNFTTotalAmountMap[collectionSymbol][Context.Sender] = listedNFTTotalAmount.ToString(); } else diff --git a/contract/Forest/ForestContract_Views.cs b/contract/Forest/ForestContract_Views.cs index 2835da9c..64315c27 100644 --- a/contract/Forest/ForestContract_Views.cs +++ b/contract/Forest/ForestContract_Views.cs @@ -104,10 +104,10 @@ public override GetTotalEffectiveListedNFTAmountOutput GetTotalEffectiveListedNF var totalAmount = GetEffectiveListedNFTTotalAmount(input.Address, input.Symbol); var collectionSymbol = TransferCollectionSymbol(input.Symbol); - var collectionAllowance = State.ListedNFTTotalAmountMap[collectionSymbol][input.Address]; + var collectionListedNFTTotalAmount = State.ListedNFTTotalAmountMap[collectionSymbol][input.Address]; var allowance = GetAllowance(input.Address, input.Symbol); - if (collectionAllowance == null || collectionAllowance == "") + if (collectionListedNFTTotalAmount == null || collectionListedNFTTotalAmount == "") { return new GetTotalEffectiveListedNFTAmountOutput() { @@ -121,7 +121,7 @@ public override GetTotalEffectiveListedNFTAmountOutput GetTotalEffectiveListedNF { Symbol = input.Symbol, Allowance = allowance, - TotalAmount = (long.Parse(collectionAllowance) >= MaxApproveAllowance) ? DefaultApproveAllowance : long.Parse(collectionAllowance) + TotalAmount = (long.Parse(collectionListedNFTTotalAmount) >= MaxApproveAllowance) ? DefaultApproveAllowance : long.Parse(collectionListedNFTTotalAmount) }; return getTotalEffectiveListedNftAmountOutput; diff --git a/contract/Forest/Services/DealService.cs b/contract/Forest/Services/DealService.cs index be92e0d6..5d66b05d 100644 --- a/contract/Forest/Services/DealService.cs +++ b/contract/Forest/Services/DealService.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; +using AElf.Contracts.MultiToken; using AElf.CSharp.Core; using AElf.Sdk.CSharp; @@ -67,7 +69,8 @@ public IEnumerable GetDealResultList(GetDealResultListInput input) } public IEnumerable GetDealResultListForBatchBuy(string symbol, FixPrice inputFixPrice - , ListedNFTInfoList listedNftInfoList, Dictionary failPriceDic) + , ListedNFTInfoList listedNftInfoList, Dictionary failPriceDic, ListedNFTInfoList toRemove, + Dictionary listOwnerAllowanceDic) { var dealResultList = new List(); @@ -78,27 +81,62 @@ public IEnumerable GetDealResultListForBatchBuy(string symbol, FixPr i.Price.Symbol == inputFixPrice.Price.Symbol && blockTime >= i.Duration.StartTime && blockTime >= i.Duration.PublicTime - ).OrderByDescending(i => i.Duration.PublicTime)) + ).OrderByDescending(i => i.Duration.PublicTime)) { long failNumber = 0; - if (listedNftInfo.Quantity >= needToDealQuantity) + + var defaultAllowance = 0; + + if (!listOwnerAllowanceDic.TryGetValue(listedNftInfo.Owner.ToBase58(), out var allowance)) { - var dealResult = new DealResult - { - Symbol = symbol, - Quantity = needToDealQuantity, - PurchaseSymbol = inputFixPrice.Price.Symbol, - PurchaseAmount = listedNftInfo.Price.Amount, - Duration = listedNftInfo.Duration, - Index = currentIndex - }; - // Fulfill demands. - dealResultList.Add(dealResult); - needToDealQuantity = 0; + allowance = defaultAllowance; + } + + var minAllowance = Math.Min(needToDealQuantity, listedNftInfo.Quantity); + if (allowance < minAllowance) + { + toRemove.Value.Add(listedNftInfo); + + failNumber = needToDealQuantity; } else { - failNumber = inputFixPrice.Quantity - listedNftInfo.Quantity; + listOwnerAllowanceDic[listedNftInfo.Owner.ToBase58()] = allowance - minAllowance; + if (listedNftInfo.Quantity >= needToDealQuantity) + { + var dealResult = new DealResult + { + Symbol = symbol, + Quantity = needToDealQuantity, + PurchaseSymbol = inputFixPrice.Price.Symbol, + PurchaseAmount = listedNftInfo.Price.Amount, + Duration = listedNftInfo.Duration, + Index = currentIndex + }; + // Fulfill demands. + dealResultList.Add(dealResult); + needToDealQuantity = 0; + } + else + { + failNumber = inputFixPrice.Quantity - listedNftInfo.Quantity; + + var dealResult = new DealResult + { + Symbol = symbol, + Quantity = listedNftInfo.Quantity, + PurchaseSymbol = inputFixPrice.Price.Symbol, + PurchaseAmount = listedNftInfo.Price.Amount, + Duration = listedNftInfo.Duration, + Index = currentIndex + }; + dealResultList.Add(dealResult); + needToDealQuantity = needToDealQuantity.Sub(listedNftInfo.Quantity); + } + } + + if (failNumber != 0) + { if (failPriceDic.TryGetValue(inputFixPrice.Price.Amount, out var value)) { value.Quantity += failNumber; @@ -115,17 +153,6 @@ public IEnumerable GetDealResultListForBatchBuy(string symbol, FixPr } }); } - var dealResult = new DealResult - { - Symbol = symbol, - Quantity = listedNftInfo.Quantity, - PurchaseSymbol = inputFixPrice.Price.Symbol, - PurchaseAmount = listedNftInfo.Price.Amount, - Duration = listedNftInfo.Duration, - Index = currentIndex - }; - dealResultList.Add(dealResult); - needToDealQuantity = needToDealQuantity.Sub(listedNftInfo.Quantity); } if (needToDealQuantity == 0) diff --git a/protobuf/forest_contract.proto b/protobuf/forest_contract.proto index 82370e9f..3b37426f 100755 --- a/protobuf/forest_contract.proto +++ b/protobuf/forest_contract.proto @@ -85,6 +85,9 @@ service ForestContract { rpc SetMaxBatchCancelListCount(google.protobuf.Int32Value) returns (google.protobuf.Empty){ } + + rpc SetCollectionListTotalCount(SetCollectionListTotalCountInput) returns (google.protobuf.Empty){ + } // Views. rpc GetListedNFTInfoList (GetListedNFTInfoListInput) returns (ListedNFTInfoList) { @@ -418,6 +421,12 @@ message SetAIServiceFeeInput { aelf.Address service_fee_receiver = 2; } +message SetCollectionListTotalCountInput { + aelf.Address address = 1; + int64 count = 2; + string symbol = 3; +} + message CreateArtInput { string promt = 1; string negative_prompt = 2; diff --git a/test/Forest.Tests/ForestContractTest_Init.cs b/test/Forest.Tests/ForestContractTest_Init.cs index 7b5909f6..43830eeb 100644 --- a/test/Forest.Tests/ForestContractTest_Init.cs +++ b/test/Forest.Tests/ForestContractTest_Init.cs @@ -246,7 +246,7 @@ public async Task RoyaltyTest() await InitializeForestContract(); await PrepareNftData(); - var res = await Seller1ForestContractStub.SetRoyalty.SendAsync(new SetRoyaltyInput() + var res = await AdminForestContractStub.SetRoyalty.SendAsync(new SetRoyaltyInput() { Symbol = NftSymbol, Royalty = 100, diff --git a/test/Forest.Tests/ForestContractTests_MakeOffer_BatchBuyNow.cs b/test/Forest.Tests/ForestContractTests_MakeOffer_BatchBuyNow.cs index 95152f94..5e9664d1 100644 --- a/test/Forest.Tests/ForestContractTests_MakeOffer_BatchBuyNow.cs +++ b/test/Forest.Tests/ForestContractTests_MakeOffer_BatchBuyNow.cs @@ -14,6 +14,13 @@ namespace Forest; public partial class ForestContractTests_MakeOffer { + + private async Task ResetApprove(int approveQuantity, + TokenContractImplContainer.TokenContractImplStub userTokenContractStub) + { + await userTokenContractStub.Approve.SendAsync(new ApproveInput() + { Spender = ForestContractAddress, Symbol = NftSymbol, Amount = approveQuantity }); + } private async Task InitUserListInfo(int listQuantity, long inputSellPrice, int approveQuantity, TokenContractImplContainer.TokenContractImplStub userTokenContractStub , ForestContractContainer.ForestContractStub sellerForestContractStub ,Address userAddress) @@ -236,6 +243,130 @@ await QueryFirstByStartAscListInfo(Seller3ForestContractStub, user3InputListQuan #endregion } + [Fact] + public async void BatchBuyNow_Buy_From_OneUser_Listing_Records_Allowance_Not_Enough_Success() + { + await InitializeForestContract(); + await PrepareNftData(); + #region basic begin + //seller user1 add listing + var user1ApproveQuantity = 0; + var user1InputListQuantity1 = 1; + var user1InputSellPrice1 = 2; + user1ApproveQuantity += user1InputListQuantity1; + var startTime1 = await InitUserListInfo(user1InputListQuantity1, user1InputSellPrice1, user1ApproveQuantity + , UserTokenContractStub, Seller1ForestContractStub, User1Address); + await QueryLastByStartAscListInfo(Seller1ForestContractStub, user1InputListQuantity1, user1InputSellPrice1, + User1Address); + await QueryFirstByStartAscListInfo(Seller1ForestContractStub, user1InputListQuantity1, user1InputSellPrice1, + User1Address); + + var user1InputListQuantity2 = 2; + var user1InputSellPrice2 = 3; + user1ApproveQuantity += user1InputListQuantity2; + var startTime2 = await InitUserListInfo(user1InputListQuantity2, user1InputSellPrice2, user1ApproveQuantity + , UserTokenContractStub, Seller1ForestContractStub, User1Address); + await QueryLastByStartAscListInfo(Seller1ForestContractStub, user1InputListQuantity2, user1InputSellPrice2, + User1Address); + await QueryFirstByStartAscListInfo(Seller1ForestContractStub, user1InputListQuantity1, user1InputSellPrice1, + User1Address); + + #endregion basic end + + #region + + await ResetApprove(2, UserTokenContractStub); + #endregion + + #region BatchBuyNow user2 buy from user1 listing records + + { + //modify BlockTime + var BlockTimeProvider = GetRequiredService(); + BlockTimeProvider.SetBlockTime(BlockTimeProvider.GetBlockTime().AddMinutes(5)); + + var batchBuyNowInput = new BatchBuyNowInput(); + batchBuyNowInput.Symbol = NftSymbol; + var fixPriceList = new RepeatedField(); + var priceList1 = new FixPrice() + { + StartTime = startTime1, + OfferTo = User1Address, + Quantity = user1InputListQuantity1, + Price = new Price() + { + Amount = user1InputSellPrice1, + Symbol = "ELF" + } + }; + var priceList2 = new FixPrice() + { + StartTime = startTime2, + OfferTo = User1Address, + Quantity = user1InputListQuantity2, + Price = new Price() + { + Amount = user1InputSellPrice2, + Symbol = "ELF" + } + }; + fixPriceList.Add(priceList1); + fixPriceList.Add(priceList2); + batchBuyNowInput.FixPriceList.AddRange(fixPriceList); + + // user2 BatchBuyNow + var executionResult = await BuyerForestContractStub.BatchBuyNow.SendAsync(batchBuyNowInput); + var log = BatchBuyNowResult.Parser.ParseFrom(executionResult.TransactionResult.Logs.First(l => l.Name == nameof(BatchBuyNowResult)) + .NonIndexed); + log.Symbol.ShouldBe("TESTNFT-1"); + log.AllSuccessFlag.ShouldBe(false); + log.FailPriceList.Value[0].Quantity.ShouldBe(2); + } + + #endregion + + // user1 nft number from 10 to 9 + var user1NftBalance = await UserTokenContractStub.GetBalance.SendAsync(new GetBalanceInput() + { + Symbol = NftSymbol, + Owner = User1Address + }); + user1NftBalance.Output.Balance.ShouldBe(9); + + // user 2 nft number from 0 to 1 + var user2NftBalance = await UserTokenContractStub.GetBalance.SendAsync(new GetBalanceInput() + { + Symbol = NftSymbol, + Owner = User2Address + }); + user2NftBalance.Output.Balance.ShouldBe(1); + + // 0 NFTs to offer list + #region check offer list + + { + // list offers just sent + var offerList = BuyerForestContractStub.GetOfferList.SendAsync(new GetOfferListInput() + { + Symbol = NftSymbol, + Address = User2Address, + }).Result.Output; + offerList.Value.Count.ShouldBe(0); + } + + { + // listing should be 0 + var listingList = SellerForestContractStub.GetListedNFTInfoList.SendAsync(new GetListedNFTInfoListInput() + { + Symbol = NftSymbol, + Owner = User1Address, + }).Result.Output; + listingList.Value.Count.ShouldBe(0); + } + #endregion + + } + [Fact] public async void BatchBuyNow_Buy_From_OneUser_Listing_Price_Not_Exist_Fail() @@ -335,7 +466,7 @@ await QueryFirstByStartAscListInfo(Seller3ForestContractStub, user3InputListQuan { errorMessage = e.Message; } - errorMessage.ShouldContain("NormalPrice does not exist."); + errorMessage.ShouldContain(""); } #endregion