diff --git a/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs b/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs index a14a4e9496..59eae99a8c 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs @@ -22,4 +22,5 @@ public static class TokenContractConstants public const string SeedCollectionSymbol = "SEED-0"; public const string SeedOwnedSymbolExternalInfoKey = "__seed_owned_symbol"; public const string SeedExpireTimeExternalInfoKey = "__seed_exp_time"; + public const int DefaultMaxBatchApproveCount = 100; } \ No newline at end of file diff --git a/contract/AElf.Contracts.MultiToken/TokenContractState.cs b/contract/AElf.Contracts.MultiToken/TokenContractState.cs index c32b0ea62f..b687cf1598 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContractState.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContractState.cs @@ -65,4 +65,6 @@ public partial class TokenContractState : ContractState public SingletonState
VoteContractAddress { get; set; } public SingletonState TokenIssuerAndOwnerModificationDisabled { get; set; } + + public SingletonState MaxBatchApproveCount { get; set; } } \ No newline at end of file diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs index 0159b7002c..2a477cafe7 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs @@ -254,14 +254,35 @@ public override Empty Approve(ApproveInput input) { AssertValidInputAddress(input.Spender); AssertValidToken(input.Symbol, input.Amount); - State.Allowances[Context.Sender][input.Spender][input.Symbol] = input.Amount; + Approve(input.Spender, input.Symbol, input.Amount); + return new Empty(); + } + + private void Approve(Address spender, string symbol, long amount) + { + State.Allowances[Context.Sender][spender][symbol] = amount; Context.Fire(new Approved { Owner = Context.Sender, - Spender = input.Spender, - Symbol = input.Symbol, - Amount = input.Amount + Spender = spender, + Symbol = symbol, + Amount = amount }); + } + + public override Empty BatchApprove(BatchApproveInput input) + { + Assert(input != null && input.Value != null && input.Value.Count > 0, "Invalid input ."); + Assert(input.Value.Count <= GetMaxBatchApproveCount(), "Exceeds the max batch approve count."); + foreach (var approve in input.Value) + { + AssertValidInputAddress(approve.Spender); + AssertValidToken(approve.Symbol, approve.Amount); + } + var approveInputList = input.Value.GroupBy(approve => approve.Symbol + approve.Spender, approve => approve) + .Select(approve => approve.Last()).ToList(); + foreach (var approve in approveInputList) + Approve(approve.Spender, approve.Symbol, approve.Amount); return new Empty(); } @@ -627,4 +648,27 @@ public override BoolValue GetTokenIssuerAndOwnerModificationEnabled(Empty input) Value = !State.TokenIssuerAndOwnerModificationDisabled.Value }; } + + public override Empty SetMaxBatchApproveCount(Int32Value input) + { + Assert(input.Value > 0, "Invalid input."); + AssertSenderAddressWith(GetDefaultParliamentController().OwnerAddress); + State.MaxBatchApproveCount.Value = input.Value; + return new Empty(); + } + + public override Int32Value GetMaxBatchApproveCount(Empty input) + { + return new Int32Value + { + Value = GetMaxBatchApproveCount() + }; + } + + private int GetMaxBatchApproveCount() + { + return State.MaxBatchApproveCount.Value == 0 + ? TokenContractConstants.DefaultMaxBatchApproveCount + : State.MaxBatchApproveCount.Value; + } } \ No newline at end of file diff --git a/protobuf/token_contract.proto b/protobuf/token_contract.proto index d858bf5f1d..e1174b8b45 100644 --- a/protobuf/token_contract.proto +++ b/protobuf/token_contract.proto @@ -40,7 +40,10 @@ service TokenContract { // enabling the Spender to call TransferFrom. rpc Approve (ApproveInput) returns (google.protobuf.Empty) { } - + + rpc BatchApprove (BatchApproveInput) returns (google.protobuf.Empty) { + } + // This is the reverse operation for Approve, it will decrease the allowance. rpc UnApprove (UnApproveInput) returns (google.protobuf.Empty) { } @@ -354,6 +357,9 @@ message ApproveInput { // The amount of token to approve. int64 amount = 3; } +message BatchApproveInput { + repeated ApproveInput value = 1; +} message UnApproveInput { // The address that allowance will be decreased. diff --git a/protobuf/token_contract_impl.proto b/protobuf/token_contract_impl.proto index 9a7779ec45..5885914e80 100644 --- a/protobuf/token_contract_impl.proto +++ b/protobuf/token_contract_impl.proto @@ -83,6 +83,11 @@ service TokenContractImpl { rpc RemoveTransactionFeeFreeAllowancesConfig (RemoveTransactionFeeFreeAllowancesConfigInput) returns (google.protobuf.Empty) { } + rpc SetMaxBatchApproveCount (google.protobuf.Int32Value) returns (google.protobuf.Empty) { + + } + + // Delegatee sets the delegation and related information of the delegator based on a transaction. rpc SetTransactionFeeDelegateInfos (SetTransactionFeeDelegateInfosInput) returns (google.protobuf.Empty){ } @@ -173,6 +178,10 @@ service TokenContractImpl { rpc GetTokenIssuerAndOwnerModificationEnabled(google.protobuf.Empty) returns (google.protobuf.BoolValue) { option (aelf.is_view) = true; } + + rpc GetMaxBatchApproveCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) { + + } } message AdvanceResourceTokenInput { diff --git a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs index 0bb4ade958..49642bae3a 100644 --- a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs +++ b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs @@ -171,6 +171,145 @@ public async Task MultiTokenContract_Approve_ContractAddress_Test() basicAllowanceOutput.Allowance.ShouldBe(2000L); } + [Fact(DisplayName = "[MultiToken] BatchApprove token to Contract")] + public async Task MultiTokenContract_BatchApprove_ContractAddress_Test() + { + await CreateTokenAndIssue(); + var approveBasisResult = (await TokenContractStub.BatchApprove.SendAsync(new BatchApproveInput + { + Value = + { + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 2000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 1000L, + Spender = OtherBasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 5000L, + Spender = TreasuryContractAddress + } + } + })).TransactionResult; + approveBasisResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var basicAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = BasicFunctionContractAddress, + Symbol = SymbolForTest + }); + basicAllowanceOutput.Allowance.ShouldBe(2000L); + var otherBasicAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = OtherBasicFunctionContractAddress, + Symbol = SymbolForTest + }); + otherBasicAllowanceOutput.Allowance.ShouldBe(1000L); + var treasuryAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = TreasuryContractAddress, + Symbol = SymbolForTest + }); + treasuryAllowanceOutput.Allowance.ShouldBe(5000L); + + approveBasisResult = (await TokenContractStub.BatchApprove.SendAsync(new BatchApproveInput + { + Value = + { + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 1000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 3000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 3000L, + Spender = TreasuryContractAddress + } + } + })).TransactionResult; + approveBasisResult.Status.ShouldBe(TransactionResultStatus.Mined); + basicAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = BasicFunctionContractAddress, + Symbol = SymbolForTest + }); + basicAllowanceOutput.Allowance.ShouldBe(3000L); + + treasuryAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = TreasuryContractAddress, + Symbol = SymbolForTest + }); + treasuryAllowanceOutput.Allowance.ShouldBe(3000L); + } + + [Fact] + public async Task MultiTokenContract_SetMaximumBatchApproveCount_Test() + { + var result = await TokenContractStub.SetMaxBatchApproveCount.SendWithExceptionAsync(new Int32Value + { + Value = 1 + }); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); + result.TransactionResult.Error.ShouldContain("Unauthorized behavior"); + var maximumBatchApproveCountOutput = await TokenContractStub.GetMaxBatchApproveCount.CallAsync(new Empty()); + maximumBatchApproveCountOutput.Value.ShouldBe(100); + var defaultParliament = await ParliamentContractStub.GetDefaultOrganizationAddress.CallAsync(new Empty()); + var proposalId = await CreateProposalAsync(TokenContractAddress, + defaultParliament, nameof(TokenContractStub.SetMaxBatchApproveCount), + new Int32Value + { + Value = 1 + }); + await ApproveWithMinersAsync(proposalId); + await ParliamentContractStub.Release.SendAsync(proposalId); + maximumBatchApproveCountOutput = await TokenContractStub.GetMaxBatchApproveCount.CallAsync(new Empty()); + maximumBatchApproveCountOutput.Value.ShouldBe(1); + await CreateTokenAndIssue(); + var approveBasisResult = (await TokenContractStub.BatchApprove.SendWithExceptionAsync(new BatchApproveInput + { + Value = + { + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 2000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 1000L, + Spender = OtherBasicFunctionContractAddress + } + } + })).TransactionResult; + approveBasisResult.Status.ShouldBe(TransactionResultStatus.Failed); + approveBasisResult.Error.ShouldContain("Exceeds the max batch approve count"); + } + [Fact(DisplayName = "[MultiToken] Approve token out of owner's balance")] public async Task MultiTokenContract_Approve_OutOfAmount_Test() {