diff --git a/contract/Forest/ForestContractState.cs b/contract/Forest/ForestContractState.cs index 7564e55..134ebcf 100755 --- a/contract/Forest/ForestContractState.cs +++ b/contract/Forest/ForestContractState.cs @@ -66,6 +66,17 @@ public partial class ForestContractState : ContractState public SingletonState MaxBatchCancelOfferCount { get; set; } public SingletonState MaxBatchCancelListCount { get; set; } + + public SingletonState TreePointsHashVerifyKey { get; set; } + /// Address -> TreePointsInfo + public MappedState TreePointsMap { get; set; } + /// Address -> timestamp 13 + public MappedState TreePointsAddTimeMap { get; set; } + /// Address -> activityId -> timestamp 13 + public MappedState TreePointsActivityClaimTimeMap { get; set; } + /// Address -> timestamp 13 + public MappedState TreePointsLevelUpgradeTimeMap { get; set; } + } } \ No newline at end of file diff --git a/contract/Forest/ForestContract_Helpers.cs b/contract/Forest/ForestContract_Helpers.cs index ca65991..a70999d 100644 --- a/contract/Forest/ForestContract_Helpers.cs +++ b/contract/Forest/ForestContract_Helpers.cs @@ -452,4 +452,12 @@ private void AssertBalanceEnoughForList(string symbol, Address address, long cur var listedAmount = GetEffectiveNFTListedTotalAmount(address, symbol); Assert(currentBalance >= (listedAmount + amount), $"The balance is not enough. Please reset it."); } + + private void CheckPointsRequestHash(string request, string hash) + { + var key = State.TreePointsHashVerifyKey.Value; + Assert(!string.IsNullOrEmpty(key), "Need SetTreePointsHashVerifyKey"); + var requestHash = HashHelper.ComputeFrom(string.Concat(request, key)); + Assert(hash.Equals(requestHash.ToHex()), "Unverified requests"); + } } diff --git a/contract/Forest/ForestContract_Tree.cs b/contract/Forest/ForestContract_Tree.cs new file mode 100644 index 0000000..af5ac3b --- /dev/null +++ b/contract/Forest/ForestContract_Tree.cs @@ -0,0 +1,163 @@ +using AElf; +using AElf.Contracts.MultiToken; +using AElf.Sdk.CSharp; +using Google.Protobuf.WellKnownTypes; + +namespace Forest; + +public partial class ForestContract +{ + public override Empty AddTreePoints(AddTreePointsInput input) + { + AssertContractInitialized(); + Assert(input != null, "Invalid param"); + Assert(!input.Address.Value.IsNullOrEmpty(), "Invalid param Address"); + Assert(input.Points > 0, "Invalid param Points"); + Assert(!string.IsNullOrEmpty(input.RequestHash), "Invalid param RequestHash"); + Assert(input.OpTime != null && input.OpTime > 0, "Invalid param OpTime"); + Assert(Context.Sender == input.Address, "Param Address is not Sender"); + + var requestStr = string.Concat(input.Address.ToBase58(), input.Points, input.PointsType, input.OpTime); + CheckPointsRequestHash(requestStr, input.RequestHash); + + var lastAddTime = State.TreePointsAddTimeMap[input.Address]; + Assert(input.OpTime > lastAddTime, "Invalid param OpTime"); + + var treePointsInfo = State.TreePointsMap[input.Address]; + if (treePointsInfo != null) + { + treePointsInfo.Points += input.Points; + } + else + { + treePointsInfo = new TreePointsInfo() + { + Owner = input.Address, + Points = input.Points, + Level = 1 + }; + } + + State.TreePointsMap[input.Address] = treePointsInfo; + State.TreePointsAddTimeMap[input.Address] = input.OpTime; + + //event + Context.Fire(new TreePointsAdded() + { + Owner = input.Address, + Points = input.Points, + PointsType = input.PointsType, + OpTime = input.OpTime, + TotalPoints = treePointsInfo.Points, + RequestHash = input.RequestHash + }); + return new Empty(); + } + public override Empty ClaimTreePoints(ClaimTreePointsInput input) + { + AssertContractInitialized(); + Assert(input != null, "Invalid param"); + Assert(!input.Address.Value.IsNullOrEmpty(), "Invalid param Address"); + Assert(input.Points >= 0, "Invalid param Points"); + Assert(!string.IsNullOrEmpty(input.ActivityId), "Invalid param ActivityId"); + Assert(!string.IsNullOrEmpty(input.RequestHash), "Invalid param RequestHash"); + Assert(input.OpTime != null && input.OpTime > 0, "Invalid param OpTime"); + Assert(input.Reward != null && !string.IsNullOrEmpty(input.Reward.Symbol) && input.Reward.Amount > 0, "Invalid param Reward"); + Assert(Context.Sender == input.Address, "Param Address is not Sender"); + + var requestStr = string.Concat(input.Address.ToBase58(), input.ActivityId,input.Points, input.OpTime); + requestStr = string.Concat(requestStr, input.Reward.Symbol, input.Reward.Amount); + CheckPointsRequestHash(requestStr, input.RequestHash); + + var lastOpTime = State.TreePointsActivityClaimTimeMap[input.Address][input.ActivityId]; + Assert(lastOpTime == 0, "You have already participated in this activity"); + Assert(input.OpTime > lastOpTime, "Invalid param OpTime"); + + var treePointsInfo = State.TreePointsMap[input.Address]; + Assert(treePointsInfo != null, "your points is zero"); + Assert(treePointsInfo.Points >= input.Points, "You don't have enough points"); + treePointsInfo.Points -= input.Points; + State.TreePointsMap[input.Address] = treePointsInfo; + State.TreePointsActivityClaimTimeMap[input.Address][input.ActivityId] = input.OpTime; + + //transfer award + var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput + { + Symbol = input.Reward.Symbol, + Owner = Context.Self + }); + Assert(balance.Balance >= input.Reward.Amount,$"The platform does not have enough {input.Reward.Symbol}"); + State.TokenContract.Transfer.Send(new TransferInput + { + To = input.Address, + Symbol = input.Reward.Symbol, + Amount = input.Reward.Amount + }); + //event + Context.Fire(new TreePointsClaimed() + { + Owner = input.Address, + Points = input.Points, + ActivityId = input.ActivityId, + OpTime = input.OpTime, + RewardSymbol = input.Reward.Symbol, + RewardAmount = input.Reward.Amount, + TotalPoints = treePointsInfo.Points, + RequestHash = input.RequestHash + }); + return new Empty(); + } + public override Empty SetTreePointsHashVerifyKey(StringValue input) + { + AssertContractInitialized(); + Assert(input != null && !string.IsNullOrEmpty(input.Value), "Invalid param key"); + var key = State.TreePointsHashVerifyKey.Value; + if (string.IsNullOrEmpty(key)) + { + State.TreePointsHashVerifyKey.Value = input.Value; + return new Empty(); + } + AssertSenderIsAdmin(); + State.TreePointsHashVerifyKey.Value = input.Value; + return new Empty(); + } + + public override Empty TreeLevelUpgrade(TreeLevelUpgradeInput input) + { + AssertContractInitialized(); + Assert(input != null, "Invalid param"); + Assert(!input.Address.Value.IsNullOrEmpty(), "Invalid param Address"); + Assert(input.Points > 0, "Invalid param Points"); + Assert(!string.IsNullOrEmpty(input.RequestHash), "Invalid param RequestHash"); + Assert(input.OpTime != null && input.OpTime > 0, "Invalid param OpTime"); + Assert(input.UpgradeLevel > 0, "Invalid UpgradeLevel, Should be greater than 0"); + Assert(Context.Sender == input.Address, "Param Address is not Sender"); + + var requestStr = string.Concat(input.Address.ToBase58(), input.Points, input.OpTime, input.UpgradeLevel); + CheckPointsRequestHash(requestStr, input.RequestHash); + + var lastOpTime = State.TreePointsLevelUpgradeTimeMap[input.Address]; + Assert(input.OpTime > lastOpTime, "Invalid param OpTime"); + + var treePointsInfo = State.TreePointsMap[input.Address]; + Assert(treePointsInfo != null, "your points is zero"); + Assert(treePointsInfo.Points >= input.Points, "You don't have enough points"); + Assert(treePointsInfo.Level < input.UpgradeLevel, $"You are already level{input.UpgradeLevel}"); + + treePointsInfo.Points -= input.Points; + State.TreePointsMap[input.Address] = treePointsInfo; + State.TreePointsLevelUpgradeTimeMap[input.Address] = input.OpTime; + + //event + Context.Fire(new TreeLevelUpgraded() + { + Owner = input.Address, + Points = input.Points, + OpTime = input.OpTime, + UpgradeLevel = input.UpgradeLevel, + TotalPoints = treePointsInfo.Points, + RequestHash = input.RequestHash + }); + return new Empty(); + } +} \ No newline at end of file diff --git a/contract/Forest/ForestContract_Views.cs b/contract/Forest/ForestContract_Views.cs index 64315c2..00843df 100644 --- a/contract/Forest/ForestContract_Views.cs +++ b/contract/Forest/ForestContract_Views.cs @@ -156,4 +156,9 @@ public override Int32Value GetMaxBatchCancelListCount(Empty input) { return new Int32Value { Value = State.MaxBatchCancelListCount.Value }; } + + public override TreePointsInfo GetTreePoints(Address input) + { + return State.TreePointsMap[input]; + } } \ No newline at end of file diff --git a/protobuf/forest_contract.proto b/protobuf/forest_contract.proto index fbb3026..4e6ba47 100755 --- a/protobuf/forest_contract.proto +++ b/protobuf/forest_contract.proto @@ -146,6 +146,22 @@ service ForestContract { rpc GetMaxBatchCancelListCount(google.protobuf.Empty) returns (google.protobuf.Int32Value){ option (aelf.is_view) = true; } + + rpc AddTreePoints(AddTreePointsInput) returns (google.protobuf.Empty){ + } + + rpc ClaimTreePoints(ClaimTreePointsInput) returns (google.protobuf.Empty){ + } + + rpc TreeLevelUpgrade(TreeLevelUpgradeInput) returns (google.protobuf.Empty){ + } + + rpc GetTreePoints(aelf.Address) returns (TreePointsInfo){ + option (aelf.is_view) = true; + } + + rpc SetTreePointsHashVerifyKey(google.protobuf.StringValue)returns (google.protobuf.Empty){ + } } // Structs. @@ -234,6 +250,11 @@ message WhitelistInfoList{ repeated WhitelistInfo whitelists = 1; } +message TreePointsInfo { + aelf.Address owner = 1; + int64 points = 2; + int64 level = 3; +} // Inputs. message InitializeInput { @@ -460,6 +481,42 @@ message CreateArtInfo { Price cost_price = 8; string painting_style = 9; } +message AddTreePointsInput { + aelf.Address address = 1; + int64 points = 2; + int32 points_type =3; + int64 op_time = 4; + string request_hash = 5; +} + +message ClaimTreePointsInput { + aelf.Address address = 1; + int64 points = 2; + string activity_id = 3; + int64 op_time = 4; + TreeReward reward = 5; + string request_hash = 6; +} + +message TreeLevelUpgradeInput { + aelf.Address address = 1; + int64 points = 2; + int64 op_time = 3; + int32 upgrade_level = 4; + string request_hash = 5; +} + +message TreeReward { + string symbol = 1; + int64 amount = 2; +} + +enum TreePointsType { + NORMALONE = 0; + NORMALTWO = 1; + INVITE = 2; +} + // Events @@ -631,3 +688,37 @@ message ArtCreated { string painting_style = 9; } +message TreePointsAdded { + option (aelf.is_event) = true; + aelf.Address owner = 1; + int64 points = 2; + int32 points_type = 3; + int64 op_time = 4; + int64 total_points = 5; + string request_hash = 6; +} + +message TreePointsClaimed { + option (aelf.is_event) = true; + aelf.Address owner = 1; + int64 points = 2; + string activity_id = 3; + int64 op_time = 4; + string reward_symbol = 5; + int64 reward_amount = 6; + int64 total_points = 7; + string request_hash = 8; +} + +message TreeLevelUpgraded { + option (aelf.is_event) = true; + aelf.Address owner = 1; + int64 points = 2; + int32 upgrade_level = 3; + int64 op_time = 4; + int64 total_points = 5; + string request_hash = 6; +} + + + diff --git a/test/Forest.Tests/ForestContractTests_TreePoints.cs b/test/Forest.Tests/ForestContractTests_TreePoints.cs new file mode 100644 index 0000000..6c24acb --- /dev/null +++ b/test/Forest.Tests/ForestContractTests_TreePoints.cs @@ -0,0 +1,205 @@ +using System.Threading.Tasks; +using AElf.Contracts.MultiToken; +using AElf.Types; +using Google.Protobuf.WellKnownTypes; +using Shouldly; +using Xunit; + +namespace Forest; + +public class ForestContractTests_TreePoints : ForestContractTestBase +{ + private const string NftSymbol = "TESTNFT-1"; + private const string NftSymbol2 = "TESTNFT-2"; + private const string ElfSymbol = "ELF"; + private const int ServiceFeeRate = 1000; // 10% + private const int AIServiceFee = 10000000; + private const string DefaultAIImageSize1024 = "1024x1024"; + private const string DefaultAIImageSize512 = "512x512"; + private const string DefaultAIImageSize256 = "256x256"; + + private async Task InitializeForestContract() + { + await AdminForestContractStub.Initialize.SendAsync(new InitializeInput + { + ServiceFeeReceiver = MarketServiceFeeReceiverAddress, + ServiceFeeRate = ServiceFeeRate, + WhitelistContractAddress = WhitelistContractAddress + }); + + await AdminForestContractStub.SetWhitelistContract.SendAsync(WhitelistContractAddress); + } + + private static Price Elf(long amunt) + { + return new Price() + { + Symbol = ElfSymbol, + Amount = amunt + }; + } + + [Fact] + public async void AddTreePoints_Test() + { + await InitializeForestContract(); + await ForestContractStub.SetTreePointsHashVerifyKey.SendAsync(new StringValue(){Value = "1a2b3c"}); + var points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(0); + await ForestContractStub.AddTreePoints.SendAsync(new AddTreePointsInput() + { + Address = DefaultAddress, + Points = 10, + PointsType = 0, + OpTime = 1730454324136, + RequestHash = "e1320b97e10afc3a37ff3df54ef811ba97b77da39972ba383b2c351bd656e4cc" + }); + points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(10); + var addResult = await ForestContractStub.AddTreePoints.SendWithExceptionAsync(new AddTreePointsInput() + { + Address = DefaultAddress, + Points = 10, + PointsType = 0, + OpTime = 1730454324136, + RequestHash = "e1320b97e10afc3a37ff3df54ef811ba97b77da39972ba383b2c351bd656e4cc" + }); + addResult.TransactionResult.Error.ShouldContain("Invalid param OpTime"); + } + [Fact] + public async void TreeLevelUpgrade_Test() + { + await InitializeForestContract(); + await ForestContractStub.SetTreePointsHashVerifyKey.SendAsync(new StringValue(){Value = "1a2b3c"}); + var points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(0); + var result = await ForestContractStub.TreeLevelUpgrade.SendWithExceptionAsync(new TreeLevelUpgradeInput() + { + Address = DefaultAddress, + Points = 100, + UpgradeLevel = 2, + OpTime = 1730462882565, + RequestHash = "d3a274f226217fc6a18c250df41f10ae8fadc30d5e933dcdad1a75a51e1d26b7" + }); + result.TransactionResult.Error.ShouldContain("your points is zero"); + + await ForestContractStub.AddTreePoints.SendAsync(new AddTreePointsInput() + { + Address = DefaultAddress, + Points = 110, + PointsType = 0, + OpTime = 1730454324136, + RequestHash = "5494bedf4cb1d69920b17d89fbc1d6c5f18e46476b347ff8aa7c843d7faf7183" + }); + + points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(110); + + result = await ForestContractStub.TreeLevelUpgrade.SendAsync(new TreeLevelUpgradeInput() + { + Address = DefaultAddress, + Points = 100, + UpgradeLevel = 2, + OpTime = 1730462882565, + RequestHash = "d3a274f226217fc6a18c250df41f10ae8fadc30d5e933dcdad1a75a51e1d26b7" + }); + points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(10); + + result = await ForestContractStub.TreeLevelUpgrade.SendWithExceptionAsync(new TreeLevelUpgradeInput() + { + Address = DefaultAddress, + Points = 100, + UpgradeLevel = 2, + OpTime = 1730462882565, + RequestHash = "d3a274f226217fc6a18c250df41f10ae8fadc30d5e933dcdad1a75a51e1d26b7" + }); + result.TransactionResult.Error.ShouldContain("Invalid param OpTime"); + } + + + [Fact] + public async void ClaimTreePoints_Test() + { + await InitializeForestContract(); + await ForestContractStub.SetTreePointsHashVerifyKey.SendAsync(new StringValue(){Value = "1a2b3c"}); + { + //transfer balance + await TokenContractStub.Transfer.SendAsync(new TransferInput() + { + To = ForestContractAddress, + Symbol = ElfSymbol, + Amount = 10000000000 + }); + + var nftBalance = await UserTokenContractStub.GetBalance.CallAsync(new GetBalanceInput() + { + Symbol = ElfSymbol, + Owner = ForestContractAddress + }); + nftBalance.Balance.ShouldBe(10000000000); + } + + + + var points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(0); + var result = await ForestContractStub.ClaimTreePoints.SendWithExceptionAsync(new ClaimTreePointsInput() + { + Address = DefaultAddress, + Points = 0, + ActivityId = "f9235130fda7230d9084374562248961d21e18e8445071b56e0630488623202e", + OpTime = 1730471642155, + Reward = new TreeReward() + { + Symbol = "ELF", + Amount = 1000000000 + }, + RequestHash = "41f1cd1ad97ea5bc7cf2513b805b899750b2d879cdf6fa9d8802b39e62f95093" + }); + result.TransactionResult.Error.ShouldContain("your points is zero"); + + await ForestContractStub.AddTreePoints.SendAsync(new AddTreePointsInput() + { + Address = DefaultAddress, + Points = 110, + PointsType = 0, + OpTime = 1730454324136, + RequestHash = "5494bedf4cb1d69920b17d89fbc1d6c5f18e46476b347ff8aa7c843d7faf7183" + }); + + points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(110); + + result = await ForestContractStub.ClaimTreePoints.SendAsync(new ClaimTreePointsInput() + { + Address = DefaultAddress, + Points = 0, + ActivityId = "f9235130fda7230d9084374562248961d21e18e8445071b56e0630488623202e", + Reward = new TreeReward() + { + Symbol = "ELF", + Amount = 1000000000 + }, + OpTime = 1730471642155, + RequestHash = "41f1cd1ad97ea5bc7cf2513b805b899750b2d879cdf6fa9d8802b39e62f95093" + }); + points = await ForestContractStub.GetTreePoints.CallAsync(DefaultAddress); + points.Points.ShouldBe(110); + + result = await ForestContractStub.ClaimTreePoints.SendWithExceptionAsync(new ClaimTreePointsInput() + { + Address = DefaultAddress, + Points = 0, + ActivityId = "f9235130fda7230d9084374562248961d21e18e8445071b56e0630488623202e", + Reward = new TreeReward() + { + Symbol = "ELF", + Amount = 1000000000 + }, + OpTime = 1730471642155, + RequestHash = "41f1cd1ad97ea5bc7cf2513b805b899750b2d879cdf6fa9d8802b39e62f95093" + }); + result.TransactionResult.Error.ShouldContain("Invalid param OpTime"); + } +} \ No newline at end of file