From 0da6487cd5c80768cbbdc14fe547f54ecf6f9970 Mon Sep 17 00:00:00 2001 From: zhifenglee-aelf Date: Fri, 7 Jun 2024 18:36:12 +0800 Subject: [PATCH 1/2] feat(simple-dao): added implementation of simple dao template --- templates/AElf.Contract.Template.csproj | 1 + .../.template.config/template.json | 21 +++ .../src/Protobuf/contract/simple_dao.proto | 95 ++++++++++++ .../src/Protobuf/message/authority_info.proto | 10 ++ .../src/Protobuf/reference/acs12.proto | 35 +++++ templates/SimpleDAOContract/src/SimpleDAO.cs | 135 +++++++++++++++++ .../SimpleDAOContract/src/SimpleDAO.csproj | 27 ++++ .../SimpleDAOContract/src/SimpleDAOState.cs | 18 +++ .../Protobuf/message/authority_info.proto | 10 ++ .../test/Protobuf/reference/acs12.proto | 35 +++++ .../test/Protobuf/stub/simple_dao.proto | 95 ++++++++++++ .../test/SimpleDAO.Tests.csproj | 51 +++++++ .../SimpleDAOContract/test/SimpleDAOTests.cs | 141 ++++++++++++++++++ templates/SimpleDAOContract/test/_Setup.cs | 31 ++++ 14 files changed, 705 insertions(+) create mode 100644 templates/SimpleDAOContract/.template.config/template.json create mode 100644 templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto create mode 100644 templates/SimpleDAOContract/src/Protobuf/message/authority_info.proto create mode 100644 templates/SimpleDAOContract/src/Protobuf/reference/acs12.proto create mode 100644 templates/SimpleDAOContract/src/SimpleDAO.cs create mode 100644 templates/SimpleDAOContract/src/SimpleDAO.csproj create mode 100644 templates/SimpleDAOContract/src/SimpleDAOState.cs create mode 100644 templates/SimpleDAOContract/test/Protobuf/message/authority_info.proto create mode 100644 templates/SimpleDAOContract/test/Protobuf/reference/acs12.proto create mode 100644 templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto create mode 100644 templates/SimpleDAOContract/test/SimpleDAO.Tests.csproj create mode 100644 templates/SimpleDAOContract/test/SimpleDAOTests.cs create mode 100644 templates/SimpleDAOContract/test/_Setup.cs diff --git a/templates/AElf.Contract.Template.csproj b/templates/AElf.Contract.Template.csproj index af3e12b..27ea2b6 100644 --- a/templates/AElf.Contract.Template.csproj +++ b/templates/AElf.Contract.Template.csproj @@ -16,6 +16,7 @@ + diff --git a/templates/SimpleDAOContract/.template.config/template.json b/templates/SimpleDAOContract/.template.config/template.json new file mode 100644 index 0000000..74fd6ed --- /dev/null +++ b/templates/SimpleDAOContract/.template.config/template.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "AElf", + "classifications": [ + "AElf/SmartContract" + ], + "identity": "AElf.Contract.SimpleDAO.Template", + "name": "AElf Contract SimpleDAO Template", + "shortName": "aelf-simple-dao", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "SimpleDAO", + "symbols": { + "NamespacePath": { + "type": "parameter", + "replaces": "AElf.Contracts.SimpleDAO" + } + } +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto b/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto new file mode 100644 index 0000000..d4c551b --- /dev/null +++ b/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto @@ -0,0 +1,95 @@ +syntax = "proto3"; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "Protobuf/reference/acs12.proto"; + +// The namespace of this class +option csharp_namespace = "AElf.Contracts.SimpleDAO"; + +service SimpleDAO { + // The name of the state class the smart contract is going to use to access + // blockchain state + option (aelf.csharp_state) = "AElf.Contracts.SimpleDAO.SimpleDAOState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + // Actions -> Methods that change state of smart contract + // This method sets up the initial state of our StackUpDAO smart contract + rpc Initialize(google.protobuf.Empty) returns (google.protobuf.Empty); + + // This method allows a user to become a member of the DAO by taking in their + // address as an input parameter + rpc JoinDAO(aelf.Address) returns (google.protobuf.Empty); + + // This method allows a user to create a proposal for other users to vote on. + // The method takes in a "CreateProposalInput" message which comprises of an + // address, a title, description and a vote threshold (i.e how many votes + // required for the proposal to pass) + rpc CreateProposal(CreateProposalInput) returns (Proposal); + + // This method allows a user to vote on proposals towards a specific proposal. + // This method takes in a "VoteInput" message which takes in the address of + // the voter, specific proposal and a boolean which represents their vote + rpc VoteOnProposal(VoteInput) returns (Proposal); + + // Views -> Methods that does not change state of smart contract + // This method allows a user to fetch a list of proposals that had been + // created by members of the DAO + rpc GetAllProposals(google.protobuf.Empty) returns (ProposalList) { + option (aelf.is_view) = true; + } + + // aelf requires explicit getter methods to access the state value, + // so we provide these three getter methods for accessing the state + // This method allows a user to fetch a proposal by proposalId + rpc GetProposal (google.protobuf.StringValue) returns (Proposal) { + option (aelf.is_view) = true; + } + + // This method allows a user to fetch the member count that joined DAO + rpc GetMemberCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) { + option (aelf.is_view) = true; + } + + // This method allows a user to check whether this member is exist by address + rpc GetMemberExist (aelf.Address) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } +} + +// Message definitions +message Member { + aelf.Address address = 1; +} + +message Proposal { + string id = 1; + string title = 2; + string description = 3; + repeated aelf.Address yesVotes = 4; + repeated aelf.Address noVotes = 5; + string status = 6; // e.g., "IN PROGRESS", "PASSED", "DENIED" + int32 voteThreshold = 7; +} + +message CreateProposalInput { + aelf.Address creator = 1; + string title = 2; + string description = 3; + int32 voteThreshold = 4; +} + +message VoteInput { + aelf.Address voter = 1; + string proposalId = 2; + bool vote = 3; // true for yes, false for no +} + +message MemberList { + repeated Member members = 1; +} + +message ProposalList { + repeated Proposal proposals = 1; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/Protobuf/message/authority_info.proto b/templates/SimpleDAOContract/src/Protobuf/message/authority_info.proto new file mode 100644 index 0000000..a20cfdc --- /dev/null +++ b/templates/SimpleDAOContract/src/Protobuf/message/authority_info.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +import "aelf/core.proto"; + +option csharp_namespace = "AElf.Contracts.SimpleDAO"; + +message AuthorityInfo { + aelf.Address contract_address = 1; + aelf.Address owner_address = 2; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/Protobuf/reference/acs12.proto b/templates/SimpleDAOContract/src/Protobuf/reference/acs12.proto new file mode 100644 index 0000000..e6ead4b --- /dev/null +++ b/templates/SimpleDAOContract/src/Protobuf/reference/acs12.proto @@ -0,0 +1,35 @@ +/** + * AElf Standards ACS12(User Contract Standard) + * + * Used to manage user contract. + */ +syntax = "proto3"; + +package acs12; + +import public "aelf/options.proto"; +import public "google/protobuf/empty.proto"; +import public "google/protobuf/wrappers.proto"; +import "aelf/core.proto"; + +option (aelf.identity) = "acs12"; +option csharp_namespace = "AElf.Standards.ACS12"; + +service UserContract{ + +} + +//Specified method fee for user contract. +message UserContractMethodFees { + // List of fees to be charged. + repeated UserContractMethodFee fees = 2; + // Optional based on the implementation of SetConfiguration method. + bool is_size_fee_free = 3; +} + +message UserContractMethodFee { + // The token symbol of the method fee. + string symbol = 1; + // The amount of fees to be charged. + int64 basic_fee = 2; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/SimpleDAO.cs b/templates/SimpleDAOContract/src/SimpleDAO.cs new file mode 100644 index 0000000..fc9798b --- /dev/null +++ b/templates/SimpleDAOContract/src/SimpleDAO.cs @@ -0,0 +1,135 @@ +using AElf.Types; +using Google.Protobuf.WellKnownTypes; + +namespace AElf.Contracts.SimpleDAO +{ + public class SimpleDAO : SimpleDAOContainer.SimpleDAOBase + { + // Initializes the DAO with a default proposal. + public override Empty Initialize(Empty input) + { + Assert(!State.Initialized.Value, "already initialized"); + var initialProposal = new Proposal + { + Id = "0", + Title = "Proposal #1", + Description = "This is the first proposal of the DAO", + Status = "IN PROGRESS", + VoteThreshold = 1, + }; + State.Proposals[initialProposal.Id] = initialProposal; + State.NextProposalId.Value = 1; + State.MemberCount.Value = 0; + + State.Initialized.Value = true; + + return new Empty(); + } + + // Allows an address to join the DAO. + public override Empty JoinDAO(Address input) + { + // Based on the address, determine whether the address has joined the DAO. If it has, throw an exception + Assert(!State.Members[input], "Member is already in the DAO"); + // If the address has not joined the DAO, then join and update the state's value to true + State.Members[input] = true; + // Read the value of MemberCount in the state, increment it by 1, and update it in the state + var currentCount = State.MemberCount.Value; + State.MemberCount.Value = currentCount + 1; + return new Empty(); + } + + // Creates a new proposal in the DAO. + public override Proposal CreateProposal(CreateProposalInput input) + { + Assert(State.Members[input.Creator], "Only DAO members can create proposals"); + var proposalId = State.NextProposalId.Value.ToString(); + var newProposal = new Proposal + { + Id = proposalId, + Title = input.Title, + Description = input.Description, + Status = "IN PROGRESS", + VoteThreshold = input.VoteThreshold, + YesVotes = { }, // Initialize as empty + NoVotes = { }, // Initialize as empty + }; + State.Proposals[proposalId] = newProposal; + State.NextProposalId.Value += 1; + return newProposal; // Ensure return + } + + // Casts a vote on a proposal. + public override Proposal VoteOnProposal(VoteInput input) + { + Assert(State.Members[input.Voter], "Only DAO members can vote"); + var proposal = State.Proposals[input.ProposalId]; // ?? new proposal + Assert(proposal != null, "Proposal not found"); + Assert( + !proposal.YesVotes.Contains(input.Voter) && !proposal.NoVotes.Contains(input.Voter), + "Member already voted" + ); + + // Add the vote to the appropriate list + if (input.Vote) + { + proposal.YesVotes.Add(input.Voter); + } + else + { + proposal.NoVotes.Add(input.Voter); + } + + // Update the proposal in state + State.Proposals[input.ProposalId] = proposal; + + // Check if the proposal has reached its vote threshold + if (proposal.YesVotes.Count >= proposal.VoteThreshold) + { + proposal.Status = "PASSED"; + } + else if (proposal.NoVotes.Count >= proposal.VoteThreshold) + { + proposal.Status = "DENIED"; + } + + return proposal; + } + + // Returns all proposals in the DAO. + public override ProposalList GetAllProposals(Empty input) + { + // Create a new list called ProposalList + var proposals = new ProposalList(); + // Start iterating through Proposals from index 0 until the value of NextProposalId, read the corresponding proposal, add it to ProposalList, and finally return ProposalList + for (var i = 0; i < State.NextProposalId.Value; i++) + { + var proposalCount = i.ToString(); + var proposal = State.Proposals[proposalCount]; + proposals.Proposals.Add(proposal); + } + return proposals; + } + + // Get information of a particular proposal. + public override Proposal GetProposal(StringValue input) + { + var proposal = State.Proposals[input.Value]; + return proposal; + } + + // Get the number of members in the DAO + public override Int32Value GetMemberCount(Empty input) + { + var memberCount = new Int32Value {Value = State.MemberCount.Value}; + return memberCount; + } + + // Check if a member exists in the DAO + public override BoolValue GetMemberExist(Address input) + { + var exist = new BoolValue {Value = State.Members[input]}; + return exist; + } + } +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/SimpleDAO.csproj b/templates/SimpleDAOContract/src/SimpleDAO.csproj new file mode 100644 index 0000000..b7799e3 --- /dev/null +++ b/templates/SimpleDAOContract/src/SimpleDAO.csproj @@ -0,0 +1,27 @@ + + + net6.0 + AElf.Contracts.SimpleDAO + true + true + + + $(MSBuildProjectDirectory)/$(BaseIntermediateOutputPath)$(Configuration)/$(TargetFramework)/ + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/SimpleDAOState.cs b/templates/SimpleDAOContract/src/SimpleDAOState.cs new file mode 100644 index 0000000..9e0d120 --- /dev/null +++ b/templates/SimpleDAOContract/src/SimpleDAOState.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AElf.Contracts.SimpleDAO; +using AElf.Sdk.CSharp.State; +using AElf.Types; + +namespace AElf.Contracts.SimpleDAO +{ + // The state class is access the blockchain state + public class SimpleDAOState : ContractState + { + public BoolState Initialized { get; set; } + public MappedState Members { get; set; } + public MappedState Proposals { get; set; } + public Int32State MemberCount { get; set; } + public Int32State NextProposalId { get; set; } + } +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/Protobuf/message/authority_info.proto b/templates/SimpleDAOContract/test/Protobuf/message/authority_info.proto new file mode 100644 index 0000000..a20cfdc --- /dev/null +++ b/templates/SimpleDAOContract/test/Protobuf/message/authority_info.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +import "aelf/core.proto"; + +option csharp_namespace = "AElf.Contracts.SimpleDAO"; + +message AuthorityInfo { + aelf.Address contract_address = 1; + aelf.Address owner_address = 2; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/Protobuf/reference/acs12.proto b/templates/SimpleDAOContract/test/Protobuf/reference/acs12.proto new file mode 100644 index 0000000..e6ead4b --- /dev/null +++ b/templates/SimpleDAOContract/test/Protobuf/reference/acs12.proto @@ -0,0 +1,35 @@ +/** + * AElf Standards ACS12(User Contract Standard) + * + * Used to manage user contract. + */ +syntax = "proto3"; + +package acs12; + +import public "aelf/options.proto"; +import public "google/protobuf/empty.proto"; +import public "google/protobuf/wrappers.proto"; +import "aelf/core.proto"; + +option (aelf.identity) = "acs12"; +option csharp_namespace = "AElf.Standards.ACS12"; + +service UserContract{ + +} + +//Specified method fee for user contract. +message UserContractMethodFees { + // List of fees to be charged. + repeated UserContractMethodFee fees = 2; + // Optional based on the implementation of SetConfiguration method. + bool is_size_fee_free = 3; +} + +message UserContractMethodFee { + // The token symbol of the method fee. + string symbol = 1; + // The amount of fees to be charged. + int64 basic_fee = 2; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto b/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto new file mode 100644 index 0000000..d4c551b --- /dev/null +++ b/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto @@ -0,0 +1,95 @@ +syntax = "proto3"; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "Protobuf/reference/acs12.proto"; + +// The namespace of this class +option csharp_namespace = "AElf.Contracts.SimpleDAO"; + +service SimpleDAO { + // The name of the state class the smart contract is going to use to access + // blockchain state + option (aelf.csharp_state) = "AElf.Contracts.SimpleDAO.SimpleDAOState"; + option (aelf.base) = "Protobuf/reference/acs12.proto"; + + // Actions -> Methods that change state of smart contract + // This method sets up the initial state of our StackUpDAO smart contract + rpc Initialize(google.protobuf.Empty) returns (google.protobuf.Empty); + + // This method allows a user to become a member of the DAO by taking in their + // address as an input parameter + rpc JoinDAO(aelf.Address) returns (google.protobuf.Empty); + + // This method allows a user to create a proposal for other users to vote on. + // The method takes in a "CreateProposalInput" message which comprises of an + // address, a title, description and a vote threshold (i.e how many votes + // required for the proposal to pass) + rpc CreateProposal(CreateProposalInput) returns (Proposal); + + // This method allows a user to vote on proposals towards a specific proposal. + // This method takes in a "VoteInput" message which takes in the address of + // the voter, specific proposal and a boolean which represents their vote + rpc VoteOnProposal(VoteInput) returns (Proposal); + + // Views -> Methods that does not change state of smart contract + // This method allows a user to fetch a list of proposals that had been + // created by members of the DAO + rpc GetAllProposals(google.protobuf.Empty) returns (ProposalList) { + option (aelf.is_view) = true; + } + + // aelf requires explicit getter methods to access the state value, + // so we provide these three getter methods for accessing the state + // This method allows a user to fetch a proposal by proposalId + rpc GetProposal (google.protobuf.StringValue) returns (Proposal) { + option (aelf.is_view) = true; + } + + // This method allows a user to fetch the member count that joined DAO + rpc GetMemberCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) { + option (aelf.is_view) = true; + } + + // This method allows a user to check whether this member is exist by address + rpc GetMemberExist (aelf.Address) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } +} + +// Message definitions +message Member { + aelf.Address address = 1; +} + +message Proposal { + string id = 1; + string title = 2; + string description = 3; + repeated aelf.Address yesVotes = 4; + repeated aelf.Address noVotes = 5; + string status = 6; // e.g., "IN PROGRESS", "PASSED", "DENIED" + int32 voteThreshold = 7; +} + +message CreateProposalInput { + aelf.Address creator = 1; + string title = 2; + string description = 3; + int32 voteThreshold = 4; +} + +message VoteInput { + aelf.Address voter = 1; + string proposalId = 2; + bool vote = 3; // true for yes, false for no +} + +message MemberList { + repeated Member members = 1; +} + +message ProposalList { + repeated Proposal proposals = 1; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/SimpleDAO.Tests.csproj b/templates/SimpleDAOContract/test/SimpleDAO.Tests.csproj new file mode 100644 index 0000000..3afcb02 --- /dev/null +++ b/templates/SimpleDAOContract/test/SimpleDAO.Tests.csproj @@ -0,0 +1,51 @@ + + + net6.0 + AElf.Contracts.SimpleDAO + + + + 0436;CS2002 + + + $(MSBuildProjectDirectory)/$(BaseIntermediateOutputPath)$(Configuration)/$(TargetFramework)/ + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + nocontract + + + nocontract + + + stub + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/SimpleDAOTests.cs b/templates/SimpleDAOContract/test/SimpleDAOTests.cs new file mode 100644 index 0000000..44fd627 --- /dev/null +++ b/templates/SimpleDAOContract/test/SimpleDAOTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Threading.Tasks; +using AElf.Types; +using Google.Protobuf.WellKnownTypes; +using Shouldly; +using Xunit; + +namespace AElf.Contracts.SimpleDAO +{ + // This class is unit test class, and it inherit TestBase. Write your unit test code inside it + public class SimpleDAOTests : TestBase + { + [Fact] + public async Task InitializeTest_Success() + { + await SimpleDAOStub.Initialize.SendAsync(new Empty()); + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue {Value = "0"}); + proposal.Title.ShouldBe("Proposal #1"); + } + + [Fact] + public async Task InitializeTest_Duplicate() + { + await SimpleDAOStub.Initialize.SendAsync(new Empty()); + var executionResult = await SimpleDAOStub.Initialize.SendWithExceptionAsync(new Empty()); + executionResult.TransactionResult.Error.ShouldContain("already initialized"); + } + + [Fact] + public async Task JoinDAOTest_Success() + { + await SimpleDAOStub.Initialize.SendAsync(new Empty()); + await SimpleDAOStub.JoinDAO.SendAsync(Accounts[1].Address); + await SimpleDAOStub.JoinDAO.SendAsync(Accounts[2].Address); + var exist1 = await SimpleDAOStub.GetMemberExist.CallAsync(Accounts[1].Address); + var exist2 = await SimpleDAOStub.GetMemberExist.CallAsync(Accounts[2].Address); + exist1.Value.ShouldBe(true); + exist2.Value.ShouldBe(true); + } + + [Fact] + public async Task JoinDAOTest_Duplicate() + { + await SimpleDAOStub.Initialize.SendAsync(new Empty()); + await SimpleDAOStub.JoinDAO.SendAsync(Accounts[1].Address); + var executionResult = await SimpleDAOStub.JoinDAO.SendWithExceptionAsync(Accounts[1].Address); + executionResult.TransactionResult.Error.ShouldContain("Member is already in the DAO"); + } + + [Fact] + public async Task CreateProposalTest_Success() + { + await JoinDAOTest_Success(); + var proposal = await CreateMockProposal(Accounts[1].Address); + proposal.Title.ShouldBe("mock_proposal"); + proposal.Id.ShouldBe("1"); + } + + private async Task CreateMockProposal(Address creator) + { + var createProposalInput = new CreateProposalInput + { + Creator = creator, + Description = "mock_proposal_desc", + Title = "mock_proposal", + VoteThreshold = 1 + }; + var proposal = await SimpleDAOStub.CreateProposal.SendAsync(createProposalInput); + return proposal.Output; + } + + [Fact] + public async Task CreateProposalTest_NoPermission() + { + await JoinDAOTest_Success(); + try + { + await CreateMockProposal(Accounts[2].Address); + } + catch (Exception e) + { + e.Message.ShouldContain("Only DAO members can create proposals"); + } + } + + [Fact] + public async Task VoteOnProposalTest_Success() + { + await JoinDAOTest_Success(); + var proposal = await CreateMockProposal(Accounts[1].Address); + var voteInput1 = new VoteInput + { + ProposalId = proposal.Id, + Vote = true, + Voter = Accounts[1].Address + }; + var voteInput2 = new VoteInput + { + ProposalId = proposal.Id, + Vote = false, + Voter = Accounts[2].Address + }; + await SimpleDAOStub.VoteOnProposal.SendAsync(voteInput1); + await SimpleDAOStub.VoteOnProposal.SendAsync(voteInput2); + var proposalResult = SimpleDAOStub.GetProposal.CallAsync(new StringValue{Value = proposal.Id}).Result; + proposalResult.YesVotes.ShouldContain(Accounts[1].Address); + proposalResult.NoVotes.ShouldContain(Accounts[2].Address); + } + + [Fact] + public async Task VoteOnProposalTest_NoPermission() + { + await JoinDAOTest_Success(); + var proposal = await CreateMockProposal(Accounts[1].Address); + var voteInput1 = new VoteInput + { + ProposalId = proposal.Id, + Vote = true, + Voter = Accounts[3].Address + }; + var executionResult = await SimpleDAOStub.VoteOnProposal.SendWithExceptionAsync(voteInput1); + executionResult.TransactionResult.Error.ShouldContain("Only DAO members can vote"); + } + + [Fact] + public async Task VoteOnProposalTest_NoProposal() + { + await JoinDAOTest_Success(); + var proposal = await CreateMockProposal(Accounts[1].Address); + var voteInput1 = new VoteInput + { + ProposalId = "123", + Vote = true, + Voter = Accounts[1].Address + }; + var executionResult = await SimpleDAOStub.VoteOnProposal.SendWithExceptionAsync(voteInput1); + executionResult.TransactionResult.Error.ShouldContain("Proposal not found"); + } + } + +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/_Setup.cs b/templates/SimpleDAOContract/test/_Setup.cs new file mode 100644 index 0000000..440ebad --- /dev/null +++ b/templates/SimpleDAOContract/test/_Setup.cs @@ -0,0 +1,31 @@ +using AElf.Cryptography.ECDSA; +using AElf.Testing.TestBase; + +namespace AElf.Contracts.SimpleDAO +{ + // The Module class load the context required for unit testing + public class Module : ContractTestModule + { + + } + + // The TestBase class inherit ContractTestBase class, it defines Stub classes and gets instances required for unit testing + public class TestBase : ContractTestBase + { + // The Stub class for unit testing + internal readonly SimpleDAOContainer.SimpleDAOStub SimpleDAOStub; + // A key pair that can be used to interact with the contract instance + private ECKeyPair DefaultKeyPair => Accounts[0].KeyPair; + + public TestBase() + { + SimpleDAOStub = GetSimpleDAOContractStub(DefaultKeyPair); + } + + private SimpleDAOContainer.SimpleDAOStub GetSimpleDAOContractStub(ECKeyPair senderKeyPair) + { + return GetTester(ContractAddress, senderKeyPair); + } + } + +} \ No newline at end of file From cc473cca0ea2e348ad30ea9f3e07d9c8bec5891c Mon Sep 17 00:00:00 2001 From: zhifenglee-aelf Date: Fri, 21 Jun 2024 16:29:56 +0800 Subject: [PATCH 2/2] feat(simple-dao-template): completed functionality for simple dao template including unit test --- .../src/ContractReference.cs | 12 + .../src/Protobuf/contract/simple_dao.proto | 73 +- .../Protobuf/reference/token_contract.proto | 855 +++++++++++++++++ templates/SimpleDAOContract/src/SimpleDAO.cs | 164 ++-- .../SimpleDAOContract/src/SimpleDAOState.cs | 9 +- .../SimpleDAOContract/src/SimpleDAO_Helper.cs | 99 ++ .../test/Protobuf/stub/simple_dao.proto | 73 +- .../test/Protobuf/stub/token_contract.proto | 855 +++++++++++++++++ .../SimpleDAOContract/test/SimpleDAOTests.cs | 880 ++++++++++++++++-- templates/SimpleDAOContract/test/_Setup.cs | 22 +- 10 files changed, 2815 insertions(+), 227 deletions(-) create mode 100644 templates/SimpleDAOContract/src/ContractReference.cs create mode 100644 templates/SimpleDAOContract/src/Protobuf/reference/token_contract.proto create mode 100644 templates/SimpleDAOContract/src/SimpleDAO_Helper.cs create mode 100644 templates/SimpleDAOContract/test/Protobuf/stub/token_contract.proto diff --git a/templates/SimpleDAOContract/src/ContractReference.cs b/templates/SimpleDAOContract/src/ContractReference.cs new file mode 100644 index 0000000..16d768a --- /dev/null +++ b/templates/SimpleDAOContract/src/ContractReference.cs @@ -0,0 +1,12 @@ +using AElf.Sdk.CSharp.State; +using AElf.Contracts.MultiToken; +using AElf.Types; + +namespace AElf.Contracts.SimpleDAO +{ + // The state class is access the blockchain state + public partial class SimpleDAOState + { + internal TokenContractContainer.TokenContractReferenceState TokenContract { get; set; } + } +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto b/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto index d4c551b..d5585f4 100644 --- a/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto +++ b/templates/SimpleDAOContract/src/Protobuf/contract/simple_dao.proto @@ -4,6 +4,7 @@ import "aelf/core.proto"; import "aelf/options.proto"; import "google/protobuf/empty.proto"; import "Protobuf/reference/acs12.proto"; +import public "google/protobuf/timestamp.proto"; // The namespace of this class option csharp_namespace = "AElf.Contracts.SimpleDAO"; @@ -16,22 +17,20 @@ service SimpleDAO { // Actions -> Methods that change state of smart contract // This method sets up the initial state of our StackUpDAO smart contract - rpc Initialize(google.protobuf.Empty) returns (google.protobuf.Empty); - - // This method allows a user to become a member of the DAO by taking in their - // address as an input parameter - rpc JoinDAO(aelf.Address) returns (google.protobuf.Empty); + rpc Initialize(InitializeInput) returns (google.protobuf.Empty); // This method allows a user to create a proposal for other users to vote on. // The method takes in a "CreateProposalInput" message which comprises of an // address, a title, description and a vote threshold (i.e how many votes // required for the proposal to pass) - rpc CreateProposal(CreateProposalInput) returns (Proposal); + rpc CreateProposal(CreateProposalInput) returns (google.protobuf.Empty); // This method allows a user to vote on proposals towards a specific proposal. // This method takes in a "VoteInput" message which takes in the address of // the voter, specific proposal and a boolean which represents their vote - rpc VoteOnProposal(VoteInput) returns (Proposal); + rpc Vote(VoteInput) returns (google.protobuf.Empty); + + rpc Withdraw(WithdrawInput) returns (google.protobuf.Empty); // Views -> Methods that does not change state of smart contract // This method allows a user to fetch a list of proposals that had been @@ -47,49 +46,65 @@ service SimpleDAO { option (aelf.is_view) = true; } - // This method allows a user to fetch the member count that joined DAO - rpc GetMemberCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) { + // get the token symbol for this DAO + rpc GetTokenSymbol (google.protobuf.Empty) returns (google.protobuf.StringValue) { option (aelf.is_view) = true; } - // This method allows a user to check whether this member is exist by address - rpc GetMemberExist (aelf.Address) returns (google.protobuf.BoolValue) { + rpc HasVoted (HasVotedInput) returns (google.protobuf.BoolValue) { option (aelf.is_view) = true; } } -// Message definitions -message Member { - aelf.Address address = 1; -} - message Proposal { string id = 1; string title = 2; string description = 3; - repeated aelf.Address yesVotes = 4; - repeated aelf.Address noVotes = 5; - string status = 6; // e.g., "IN PROGRESS", "PASSED", "DENIED" - int32 voteThreshold = 7; + string status = 4; // e.g., "IN PROGRESS", "PASSED", "DENIED" + aelf.Address proposer = 5; + google.protobuf.Timestamp start_timestamp = 6; + google.protobuf.Timestamp end_timestamp = 7; + ProposalResult result = 8; +} + +message ProposalResult { + int64 approve_counts = 1; + int64 reject_counts = 2; + int64 abstain_counts = 3; } message CreateProposalInput { - aelf.Address creator = 1; - string title = 2; - string description = 3; - int32 voteThreshold = 4; + string title = 1; + string description = 2; + google.protobuf.Timestamp start_timestamp = 3; + google.protobuf.Timestamp end_timestamp = 4; +} + +enum VoteOption { + APPROVED = 0; + REJECTED = 1; + ABSTAINED = 2; } message VoteInput { - aelf.Address voter = 1; - string proposalId = 2; - bool vote = 3; // true for yes, false for no + string proposalId = 1; + VoteOption vote = 2; + int64 amount = 3; } -message MemberList { - repeated Member members = 1; +message InitializeInput { + string tokenSymbol = 1; +} + +message WithdrawInput { + string proposalId = 1; } message ProposalList { repeated Proposal proposals = 1; +} + +message HasVotedInput { + string proposalId = 1; + aelf.Address address = 2; } \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/Protobuf/reference/token_contract.proto b/templates/SimpleDAOContract/src/Protobuf/reference/token_contract.proto new file mode 100644 index 0000000..6d73c0b --- /dev/null +++ b/templates/SimpleDAOContract/src/Protobuf/reference/token_contract.proto @@ -0,0 +1,855 @@ +/** + * MultiToken contract. + */ +syntax = "proto3"; + +package token; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; + +option csharp_namespace = "AElf.Contracts.MultiToken"; + +service TokenContract { + // Create a new token. + rpc Create (CreateInput) returns (google.protobuf.Empty) { + } + + // Issuing some amount of tokens to an address is the action of increasing that addresses balance + // for the given token. The total amount of issued tokens must not exceed the total supply of the token + // and only the issuer (creator) of the token can issue tokens. + // Issuing tokens effectively increases the circulating supply. + rpc Issue (IssueInput) returns (google.protobuf.Empty) { + } + + // Transferring tokens simply is the action of transferring a given amount of tokens from one address to another. + // The origin or source address is the signer of the transaction. + // The balance of the sender must be higher than the amount that is transferred. + rpc Transfer (TransferInput) returns (google.protobuf.Empty) { + } + + // The TransferFrom action will transfer a specified amount of tokens from one address to another. + // For this operation to succeed the from address needs to have approved (see allowances) enough tokens + // to Sender of this transaction. If successful the amount will be removed from the allowance. + rpc TransferFrom (TransferFromInput) returns (google.protobuf.Empty) { + } + + // The approve action increases the allowance from the Sender to the Spender address, + // enabling the Spender to call TransferFrom. + rpc Approve (ApproveInput) returns (google.protobuf.Empty) { + } + + // This is the reverse operation for Approve, it will decrease the allowance. + rpc UnApprove (UnApproveInput) returns (google.protobuf.Empty) { + } + + // This method can be used to lock tokens. + rpc Lock (LockInput) returns (google.protobuf.Empty) { + } + + // This is the reverse operation of locking, it un-locks some previously locked tokens. + rpc Unlock (UnlockInput) returns (google.protobuf.Empty) { + } + + // This action will burn the specified amount of tokens, removing them from the token’s Supply. + rpc Burn (BurnInput) returns (google.protobuf.Empty) { + } + + // Set the primary token of side chain. + rpc SetPrimaryTokenSymbol (SetPrimaryTokenSymbolInput) returns (google.protobuf.Empty) { + } + + // This interface is used for cross-chain transfer. + rpc CrossChainTransfer (CrossChainTransferInput) returns (google.protobuf.Empty) { + } + + // This method is used to receive cross-chain transfers. + rpc CrossChainReceiveToken (CrossChainReceiveTokenInput) returns (google.protobuf.Empty) { + } + + // The side chain creates tokens. + rpc CrossChainCreateToken(CrossChainCreateTokenInput) returns (google.protobuf.Empty) { + } + + // When the side chain is started, the side chain is initialized with the parent chain information. + rpc InitializeFromParentChain (InitializeFromParentChainInput) returns (google.protobuf.Empty) { + } + + // Handle the transaction fees charged by ChargeTransactionFees. + rpc ClaimTransactionFees (TotalTransactionFeesMap) returns (google.protobuf.Empty) { + } + + // Used to collect transaction fees. + rpc ChargeTransactionFees (ChargeTransactionFeesInput) returns (ChargeTransactionFeesOutput) { + } + + rpc ChargeUserContractTransactionFees(ChargeTransactionFeesInput) returns(ChargeTransactionFeesOutput){ + + } + + // Check the token threshold. + rpc CheckThreshold (CheckThresholdInput) returns (google.protobuf.Empty) { + } + + // Initialize coefficients of every type of tokens supporting charging fee. + rpc InitialCoefficients (google.protobuf.Empty) returns (google.protobuf.Empty){ + } + + // Processing resource token received. + rpc DonateResourceToken (TotalResourceTokensMaps) returns (google.protobuf.Empty) { + } + + // A transaction resource fee is charged to implement the ACS8 standards. + rpc ChargeResourceToken (ChargeResourceTokenInput) returns (google.protobuf.Empty) { + } + + // Verify that the resource token are sufficient. + rpc CheckResourceToken (google.protobuf.Empty) returns (google.protobuf.Empty) { + } + + // Set the list of tokens to pay transaction fees. + rpc SetSymbolsToPayTxSizeFee (SymbolListToPayTxSizeFee) returns (google.protobuf.Empty){ + } + + // Update the coefficient of the transaction fee calculation formula. + rpc UpdateCoefficientsForSender (UpdateCoefficientsInput) returns (google.protobuf.Empty) { + } + + // Update the coefficient of the transaction fee calculation formula. + rpc UpdateCoefficientsForContract (UpdateCoefficientsInput) returns (google.protobuf.Empty) { + } + + // This method is used to initialize the governance organization for some functions, + // including: the coefficient of the user transaction fee calculation formula, + // the coefficient of the contract developer resource fee calculation formula, and the side chain rental fee. + rpc InitializeAuthorizedController (google.protobuf.Empty) returns (google.protobuf.Empty){ + } + + rpc AddAddressToCreateTokenWhiteList (aelf.Address) returns (google.protobuf.Empty) { + } + rpc RemoveAddressFromCreateTokenWhiteList (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc SetTransactionFeeDelegations (SetTransactionFeeDelegationsInput) returns (SetTransactionFeeDelegationsOutput){ + } + + rpc RemoveTransactionFeeDelegator (RemoveTransactionFeeDelegatorInput) returns (google.protobuf.Empty){ + } + + rpc RemoveTransactionFeeDelegatee (RemoveTransactionFeeDelegateeInput) returns (google.protobuf.Empty){ + } + + // Get all delegatees' address of delegator from input + rpc GetTransactionFeeDelegatees (GetTransactionFeeDelegateesInput) returns (GetTransactionFeeDelegateesOutput) { + option (aelf.is_view) = true; + } + + // Query token information. + rpc GetTokenInfo (GetTokenInfoInput) returns (TokenInfo) { + option (aelf.is_view) = true; + } + + // Query native token information. + rpc GetNativeTokenInfo (google.protobuf.Empty) returns (TokenInfo) { + option (aelf.is_view) = true; + } + + // Query resource token information. + rpc GetResourceTokenInfo (google.protobuf.Empty) returns (TokenInfoList) { + option (aelf.is_view) = true; + } + + // Query the balance at the specified address. + rpc GetBalance (GetBalanceInput) returns (GetBalanceOutput) { + option (aelf.is_view) = true; + } + + // Query the account's allowance for other addresses + rpc GetAllowance (GetAllowanceInput) returns (GetAllowanceOutput) { + option (aelf.is_view) = true; + } + + // Check whether the token is in the whitelist of an address, + // which can be called TransferFrom to transfer the token under the condition of not being credited. + rpc IsInWhiteList (IsInWhiteListInput) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } + + // Query the information for a lock. + rpc GetLockedAmount (GetLockedAmountInput) returns (GetLockedAmountOutput) { + option (aelf.is_view) = true; + } + + // Query the address of receiving token in cross-chain transfer. + rpc GetCrossChainTransferTokenContractAddress (GetCrossChainTransferTokenContractAddressInput) returns (aelf.Address) { + option (aelf.is_view) = true; + } + + // Query the name of the primary Token. + rpc GetPrimaryTokenSymbol (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + // Query the coefficient of the transaction fee calculation formula. + rpc GetCalculateFeeCoefficientsForContract (google.protobuf.Int32Value) returns (CalculateFeeCoefficients) { + option (aelf.is_view) = true; + } + + // Query the coefficient of the transaction fee calculation formula. + rpc GetCalculateFeeCoefficientsForSender (google.protobuf.Empty) returns (CalculateFeeCoefficients) { + option (aelf.is_view) = true; + } + + // Query tokens that can pay transaction fees. + rpc GetSymbolsToPayTxSizeFee (google.protobuf.Empty) returns (SymbolListToPayTxSizeFee){ + option (aelf.is_view) = true; + } + + // Query the hash of the last input of ClaimTransactionFees. + rpc GetLatestTotalTransactionFeesMapHash (google.protobuf.Empty) returns (aelf.Hash){ + option (aelf.is_view) = true; + } + + // Query the hash of the last input of DonateResourceToken. + rpc GetLatestTotalResourceTokensMapsHash (google.protobuf.Empty) returns (aelf.Hash){ + option (aelf.is_view) = true; + } + rpc IsTokenAvailableForMethodFee (google.protobuf.StringValue) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } + rpc GetReservedExternalInfoKeyList (google.protobuf.Empty) returns (StringList) { + option (aelf.is_view) = true; + } + + rpc GetTransactionFeeDelegationsOfADelegatee(GetTransactionFeeDelegationsOfADelegateeInput) returns(TransactionFeeDelegations){ + option (aelf.is_view) = true; + } +} + +message TokenInfo { + // The symbol of the token.f + string symbol = 1; + // The full name of the token. + string token_name = 2; + // The current supply of the token. + int64 supply = 3; + // The total supply of the token. + int64 total_supply = 4; + // The precision of the token. + int32 decimals = 5; + // The address that has permission to issue the token. + aelf.Address issuer = 6; + // A flag indicating if this token is burnable. + bool is_burnable = 7; + // The chain id of the token. + int32 issue_chain_id = 8; + // The amount of issued tokens. + int64 issued = 9; + // The external information of the token. + ExternalInfo external_info = 10; + // The address that owns the token. + aelf.Address owner = 11; +} + +message ExternalInfo { + map value = 1; +} + +message CreateInput { + // The symbol of the token. + string symbol = 1; + // The full name of the token. + string token_name = 2; + // The total supply of the token. + int64 total_supply = 3; + // The precision of the token + int32 decimals = 4; + // The address that has permission to issue the token. + aelf.Address issuer = 5; + // A flag indicating if this token is burnable. + bool is_burnable = 6; + // A whitelist address list used to lock tokens. + repeated aelf.Address lock_white_list = 7; + // The chain id of the token. + int32 issue_chain_id = 8; + // The external information of the token. + ExternalInfo external_info = 9; + // The address that owns the token. + aelf.Address owner = 10; +} + +message SetPrimaryTokenSymbolInput { + // The symbol of the token. + string symbol = 1; +} + +message IssueInput { + // The token symbol to issue. + string symbol = 1; + // The token amount to issue. + int64 amount = 2; + // The memo. + string memo = 3; + // The target address to issue. + aelf.Address to = 4; +} + +message TransferInput { + // The receiver of the token. + aelf.Address to = 1; + // The token symbol to transfer. + string symbol = 2; + // The amount to to transfer. + int64 amount = 3; + // The memo. + string memo = 4; +} + +message LockInput { + // The one want to lock his token. + aelf.Address address = 1; + // Id of the lock. + aelf.Hash lock_id = 2; + // The symbol of the token to lock. + string symbol = 3; + // a memo. + string usage = 4; + // The amount of tokens to lock. + int64 amount = 5; +} + +message UnlockInput { + // The one want to un-lock his token. + aelf.Address address = 1; + // Id of the lock. + aelf.Hash lock_id = 2; + // The symbol of the token to un-lock. + string symbol = 3; + // a memo. + string usage = 4; + // The amount of tokens to un-lock. + int64 amount = 5; +} + +message TransferFromInput { + // The source address of the token. + aelf.Address from = 1; + // The destination address of the token. + aelf.Address to = 2; + // The symbol of the token to transfer. + string symbol = 3; + // The amount to transfer. + int64 amount = 4; + // The memo. + string memo = 5; +} + +message ApproveInput { + // The address that allowance will be increased. + aelf.Address spender = 1; + // The symbol of token to approve. + string symbol = 2; + // The amount of token to approve. + int64 amount = 3; +} + +message UnApproveInput { + // The address that allowance will be decreased. + aelf.Address spender = 1; + // The symbol of token to un-approve. + string symbol = 2; + // The amount of token to un-approve. + int64 amount = 3; +} + +message BurnInput { + // The symbol of token to burn. + string symbol = 1; + // The amount of token to burn. + int64 amount = 2; +} + +message ChargeResourceTokenInput { + // Collection of charge resource token, Symbol->Amount. + map cost_dic = 1; + // The sender of the transaction. + aelf.Address caller = 2; +} + +message TransactionFeeBill { + // The transaction fee dictionary, Symbol->fee. + map fees_map = 1; +} + +message TransactionFreeFeeAllowanceBill { + // The transaction free fee allowance dictionary, Symbol->fee. + map free_fee_allowances_map = 1; +} + +message CheckThresholdInput { + // The sender of the transaction. + aelf.Address sender = 1; + // The threshold to set, Symbol->Threshold. + map symbol_to_threshold = 2; + // Whether to check the allowance. + bool is_check_allowance = 3; +} + +message GetTokenInfoInput { + // The symbol of token. + string symbol = 1; +} + +message GetBalanceInput { + // The symbol of token. + string symbol = 1; + // The target address of the query. + aelf.Address owner = 2; +} + +message GetBalanceOutput { + // The symbol of token. + string symbol = 1; + // The target address of the query. + aelf.Address owner = 2; + // The balance of the owner. + int64 balance = 3; +} + +message GetAllowanceInput { + // The symbol of token. + string symbol = 1; + // The address of the token owner. + aelf.Address owner = 2; + // The address of the spender. + aelf.Address spender = 3; +} + +message GetAllowanceOutput { + // The symbol of token. + string symbol = 1; + // The address of the token owner. + aelf.Address owner = 2; + // The address of the spender. + aelf.Address spender = 3; + // The amount of allowance. + int64 allowance = 4; +} + +message CrossChainTransferInput { + // The receiver of transfer. + aelf.Address to = 1; + // The symbol of token. + string symbol = 2; + // The amount of token to transfer. + int64 amount = 3; + // The memo. + string memo = 4; + // The destination chain id. + int32 to_chain_id = 5; + // The chain id of the token. + int32 issue_chain_id = 6; +} + +message CrossChainReceiveTokenInput { + // The source chain id. + int32 from_chain_id = 1; + // The height of the transfer transaction. + int64 parent_chain_height = 2; + // The raw bytes of the transfer transaction. + bytes transfer_transaction_bytes = 3; + // The merkle path created from the transfer transaction. + aelf.MerklePath merkle_path = 4; +} + +message IsInWhiteListInput { + // The symbol of token. + string symbol = 1; + // The address to check. + aelf.Address address = 2; +} + +message SymbolToPayTxSizeFee{ + // The symbol of token. + string token_symbol = 1; + // The charge weight of primary token. + int32 base_token_weight = 2; + // The new added token charge weight. For example, the charge weight of primary Token is set to 1. + // The newly added token charge weight is set to 10. If the transaction requires 1 unit of primary token, + // the user can also pay for 10 newly added tokens. + int32 added_token_weight = 3; +} + +message SymbolListToPayTxSizeFee{ + // Transaction fee token information. + repeated SymbolToPayTxSizeFee symbols_to_pay_tx_size_fee = 1; +} + +message ChargeTransactionFeesInput { + // The method name of transaction. + string method_name = 1; + // The contract address of transaction. + aelf.Address contract_address = 2; + // The amount of transaction size fee. + int64 transaction_size_fee = 3; + // Transaction fee token information. + repeated SymbolToPayTxSizeFee symbols_to_pay_tx_size_fee = 4; +} + +message ChargeTransactionFeesOutput { + // Whether the charge was successful. + bool success = 1; + // The charging information. + string charging_information = 2; +} + +message CallbackInfo { + aelf.Address contract_address = 1; + string method_name = 2; +} + +message ExtraTokenListModified { + option (aelf.is_event) = true; + // Transaction fee token information. + SymbolListToPayTxSizeFee symbol_list_to_pay_tx_size_fee = 1; +} + +message GetLockedAmountInput { + // The address of the lock. + aelf.Address address = 1; + // The token symbol. + string symbol = 2; + // The id of the lock. + aelf.Hash lock_id = 3; +} + +message GetLockedAmountOutput { + // The address of the lock. + aelf.Address address = 1; + // The token symbol. + string symbol = 2; + // The id of the lock. + aelf.Hash lock_id = 3; + // The locked amount. + int64 amount = 4; +} + +message TokenInfoList { + // List of token information. + repeated TokenInfo value = 1; +} + +message GetCrossChainTransferTokenContractAddressInput { + // The chain id. + int32 chainId = 1; +} + +message CrossChainCreateTokenInput { + // The chain id of the chain on which the token was created. + int32 from_chain_id = 1; + // The height of the transaction that created the token. + int64 parent_chain_height = 2; + // The transaction that created the token. + bytes transaction_bytes = 3; + // The merkle path created from the transaction that created the transaction. + aelf.MerklePath merkle_path = 4; +} + +message InitializeFromParentChainInput { + // The amount of resource. + map resource_amount = 1; + // The token contract addresses. + map registered_other_token_contract_addresses = 2; + // The creator the side chain. + aelf.Address creator = 3; +} + +message UpdateCoefficientsInput { + // The specify pieces gonna update. + repeated int32 piece_numbers = 1; + // Coefficients of one single type. + CalculateFeeCoefficients coefficients = 2; +} + +enum FeeTypeEnum { + READ = 0; + STORAGE = 1; + WRITE = 2; + TRAFFIC = 3; + TX = 4; +} + +message CalculateFeePieceCoefficients { + // Coefficients of one single piece. + // The first char is its type: liner / power. + // The second char is its piece upper bound. + repeated int32 value = 1; +} + +message CalculateFeeCoefficients { + // The resource fee type, like READ, WRITE, etc. + int32 fee_token_type = 1; + // Coefficients of one single piece. + repeated CalculateFeePieceCoefficients piece_coefficients_list = 2; +} + +message AllCalculateFeeCoefficients { + // The coefficients of fee Calculation. + repeated CalculateFeeCoefficients value = 1; +} + +message TotalTransactionFeesMap +{ + // Token dictionary that charge transaction fee, Symbol->Amount. + map value = 1; + // The hash of the block processing the transaction. + aelf.Hash block_hash = 2; + // The height of the block processing the transaction. + int64 block_height = 3; +} + +message TotalResourceTokensMaps { + // Resource tokens to charge. + repeated ContractTotalResourceTokens value = 1; + // The hash of the block processing the transaction. + aelf.Hash block_hash = 2; + // The height of the block processing the transaction. + int64 block_height = 3; +} + +message ContractTotalResourceTokens { + // The contract address. + aelf.Address contract_address = 1; + // Resource tokens to charge. + TotalResourceTokensMap tokens_map = 2; +} + +message TotalResourceTokensMap +{ + // Resource token dictionary, Symbol->Amount. + map value = 1; +} + +message StringList { + repeated string value = 1; +} + +message TransactionFeeDelegations{ + // delegation, symbols and its' amount + map delegations = 1; + // height when added + int64 block_height = 2; + //Whether to pay transaction fee continuously + bool isUnlimitedDelegate = 3; +} + +message TransactionFeeDelegatees{ + map delegatees = 1; +} + +message SetTransactionFeeDelegationsInput { + // the delegator address + aelf.Address delegator_address = 1; + // delegation, symbols and its' amount + map delegations = 2; +} + +message SetTransactionFeeDelegationsOutput { + bool success = 1; +} + +message RemoveTransactionFeeDelegatorInput{ + // the delegator address + aelf.Address delegator_address = 1; +} + +message RemoveTransactionFeeDelegateeInput { + // the delegatee address + aelf.Address delegatee_address = 1; +} + +message GetTransactionFeeDelegationsOfADelegateeInput { + aelf.Address delegatee_address = 1; + aelf.Address delegator_address = 2; +} + +message GetTransactionFeeDelegateesInput { + aelf.Address delegator_address = 1; +} + +message GetTransactionFeeDelegateesOutput { + repeated aelf.Address delegatee_addresses = 1; +} + +// Events + +message Transferred { + option (aelf.is_event) = true; + // The source address of the transferred token. + aelf.Address from = 1 [(aelf.is_indexed) = true]; + // The destination address of the transferred token. + aelf.Address to = 2 [(aelf.is_indexed) = true]; + // The symbol of the transferred token. + string symbol = 3 [(aelf.is_indexed) = true]; + // The amount of the transferred token. + int64 amount = 4; + // The memo. + string memo = 5; +} + +message Approved { + option (aelf.is_event) = true; + // The address of the token owner. + aelf.Address owner = 1 [(aelf.is_indexed) = true]; + // The address that allowance be increased. + aelf.Address spender = 2 [(aelf.is_indexed) = true]; + // The symbol of approved token. + string symbol = 3 [(aelf.is_indexed) = true]; + // The amount of approved token. + int64 amount = 4; +} + +message UnApproved { + option (aelf.is_event) = true; + // The address of the token owner. + aelf.Address owner = 1 [(aelf.is_indexed) = true]; + // The address that allowance be decreased. + aelf.Address spender = 2 [(aelf.is_indexed) = true]; + // The symbol of un-approved token. + string symbol = 3 [(aelf.is_indexed) = true]; + // The amount of un-approved token. + int64 amount = 4; +} + +message Burned +{ + option (aelf.is_event) = true; + // The address who wants to burn token. + aelf.Address burner = 1 [(aelf.is_indexed) = true]; + // The symbol of burned token. + string symbol = 2 [(aelf.is_indexed) = true]; + // The amount of burned token. + int64 amount = 3; +} + +message ChainPrimaryTokenSymbolSet { + option (aelf.is_event) = true; + // The symbol of token. + string token_symbol = 1; +} + +message CalculateFeeAlgorithmUpdated { + option (aelf.is_event) = true; + // All calculate fee coefficients after modification. + AllCalculateFeeCoefficients all_type_fee_coefficients = 1; +} + +message RentalCharged { + option (aelf.is_event) = true; + // The symbol of rental fee charged. + string symbol = 1; + // The amount of rental fee charged. + int64 amount = 2; + // The payer of rental fee. + aelf.Address payer = 3; + // The receiver of rental fee. + aelf.Address receiver = 4; +} + +message RentalAccountBalanceInsufficient { + option (aelf.is_event) = true; + // The symbol of insufficient rental account balance. + string symbol = 1; + // The balance of the account. + int64 amount = 2; +} + +message TokenCreated { + option (aelf.is_event) = true; + // The symbol of the token. + string symbol = 1; + // The full name of the token. + string token_name = 2; + // The total supply of the token. + int64 total_supply = 3; + // The precision of the token. + int32 decimals = 4; + // The address that has permission to issue the token. + aelf.Address issuer = 5; + // A flag indicating if this token is burnable. + bool is_burnable = 6; + // The chain id of the token. + int32 issue_chain_id = 7; + // The external information of the token. + ExternalInfo external_info = 8; + // The address that owns the token. + aelf.Address owner = 9; +} + +message Issued { + option (aelf.is_event) = true; + // The symbol of issued token. + string symbol = 1; + // The amount of issued token. + int64 amount = 2; + // The memo. + string memo = 3; + // The issued target address. + aelf.Address to = 4; +} + +message CrossChainTransferred { + option (aelf.is_event) = true; + // The source address of the transferred token. + aelf.Address from = 1; + // The destination address of the transferred token. + aelf.Address to = 2; + // The symbol of the transferred token. + string symbol = 3; + // The amount of the transferred token. + int64 amount = 4; + // The memo. + string memo = 5; + // The destination chain id. + int32 to_chain_id = 6; + // The chain id of the token. + int32 issue_chain_id = 7; +} + +message CrossChainReceived { + option (aelf.is_event) = true; + // The source address of the transferred token. + aelf.Address from = 1; + // The destination address of the transferred token. + aelf.Address to = 2; + // The symbol of the received token. + string symbol = 3; + // The amount of the received token. + int64 amount = 4; + // The memo. + string memo = 5; + // The destination chain id. + int32 from_chain_id = 6; + // The chain id of the token. + int32 issue_chain_id = 7; + // The parent chain height of the transfer transaction. + int64 parent_chain_height = 8; + // The id of transfer transaction. + aelf.Hash transfer_transaction_id =9; +} + +message TransactionFeeDelegationAdded { + option (aelf.is_event) = true; + aelf.Address delegator = 1 [(aelf.is_indexed) = true]; + aelf.Address delegatee = 2 [(aelf.is_indexed) = true]; + aelf.Address caller = 3 [(aelf.is_indexed) = true]; +} + +message TransactionFeeDelegationCancelled { + option (aelf.is_event) = true; + aelf.Address delegator = 1 [(aelf.is_indexed) = true]; + aelf.Address delegatee = 2 [(aelf.is_indexed) = true]; + aelf.Address caller = 3 [(aelf.is_indexed) = true]; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/SimpleDAO.cs b/templates/SimpleDAOContract/src/SimpleDAO.cs index fc9798b..d6122ae 100644 --- a/templates/SimpleDAOContract/src/SimpleDAO.cs +++ b/templates/SimpleDAOContract/src/SimpleDAO.cs @@ -1,99 +1,92 @@ -using AElf.Types; +using AElf.Contracts.MultiToken; +using AElf.Sdk.CSharp; using Google.Protobuf.WellKnownTypes; namespace AElf.Contracts.SimpleDAO { - public class SimpleDAO : SimpleDAOContainer.SimpleDAOBase + public partial class SimpleDAO : SimpleDAOContainer.SimpleDAOBase { - // Initializes the DAO with a default proposal. - public override Empty Initialize(Empty input) + private const int StartProposalId = 1; + + // Initializes the DAO with a default proposal. Members are defined by token holders. + public override Empty Initialize(InitializeInput input) { Assert(!State.Initialized.Value, "already initialized"); - var initialProposal = new Proposal + + State.TokenContract.Value = Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName); + Assert(State.TokenContract.Value != null, "Cannot find token contract!"); + + var tokenInfo = State.TokenContract.GetTokenInfo.Call(new GetTokenInfoInput { - Id = "0", - Title = "Proposal #1", - Description = "This is the first proposal of the DAO", - Status = "IN PROGRESS", - VoteThreshold = 1, - }; - State.Proposals[initialProposal.Id] = initialProposal; - State.NextProposalId.Value = 1; - State.MemberCount.Value = 0; + Symbol = input.TokenSymbol + }); + Assert(!string.IsNullOrEmpty(tokenInfo.Symbol), $"Token {input.TokenSymbol} not found"); + State.TokenSymbol.Value = input.TokenSymbol; + State.NextProposalId.Value = StartProposalId; State.Initialized.Value = true; return new Empty(); } - // Allows an address to join the DAO. - public override Empty JoinDAO(Address input) - { - // Based on the address, determine whether the address has joined the DAO. If it has, throw an exception - Assert(!State.Members[input], "Member is already in the DAO"); - // If the address has not joined the DAO, then join and update the state's value to true - State.Members[input] = true; - // Read the value of MemberCount in the state, increment it by 1, and update it in the state - var currentCount = State.MemberCount.Value; - State.MemberCount.Value = currentCount + 1; - return new Empty(); - } - - // Creates a new proposal in the DAO. - public override Proposal CreateProposal(CreateProposalInput input) + // Creates a new proposal in the DAO. Anyone can create proposals, even non-members. + public override Empty CreateProposal(CreateProposalInput input) { - Assert(State.Members[input.Creator], "Only DAO members can create proposals"); + Assert(!string.IsNullOrEmpty(input.Title), "Title should not be empty."); + Assert(!string.IsNullOrEmpty(input.Description), "Description should not be empty."); + Assert(input.StartTimestamp >= Context.CurrentBlockTime, "Start time should be greater or equal to current block time."); + Assert(input.EndTimestamp > Context.CurrentBlockTime, "Expire time should be greater than current block time."); + var proposalId = State.NextProposalId.Value.ToString(); - var newProposal = new Proposal - { - Id = proposalId, - Title = input.Title, - Description = input.Description, - Status = "IN PROGRESS", - VoteThreshold = input.VoteThreshold, - YesVotes = { }, // Initialize as empty - NoVotes = { }, // Initialize as empty + + var newProposal = new Proposal { + Id = proposalId, + Title = input.Title, + Description = input.Description, + Proposer = Context.Sender, + StartTimestamp = input.StartTimestamp, + EndTimestamp = input.EndTimestamp, + Result = new ProposalResult + { + ApproveCounts = 0, + RejectCounts = 0, + AbstainCounts = 0 + } }; State.Proposals[proposalId] = newProposal; + State.NextProposalId.Value += 1; - return newProposal; // Ensure return + return new Empty(); } - // Casts a vote on a proposal. - public override Proposal VoteOnProposal(VoteInput input) + // Casts a vote on a proposal. Only members can vote. + public override Empty Vote(VoteInput input) { - Assert(State.Members[input.Voter], "Only DAO members can vote"); - var proposal = State.Proposals[input.ProposalId]; // ?? new proposal - Assert(proposal != null, "Proposal not found"); - Assert( - !proposal.YesVotes.Contains(input.Voter) && !proposal.NoVotes.Contains(input.Voter), - "Member already voted" - ); - - // Add the vote to the appropriate list - if (input.Vote) - { - proposal.YesVotes.Add(input.Voter); - } - else - { - proposal.NoVotes.Add(input.Voter); - } - - // Update the proposal in state - State.Proposals[input.ProposalId] = proposal; + AssertProposalExists(input.ProposalId); + var proposal = GetProposal(input.ProposalId); + Assert(proposal.StartTimestamp <= Context.CurrentBlockTime, $"Proposal {proposal.Id} has not started. Voting is not allowed."); + Assert(proposal.EndTimestamp > Context.CurrentBlockTime, $"Proposal {proposal.Id} has ended. Voting is not allowed."); + var amount = input.Amount; + Assert(amount > 0, "Amount must be greater than 0"); + Assert(State.Voters[proposal.Id][Context.Sender] == false, "You have already voted."); + + TransferTokensForProposalBallot(proposal.Id, amount); - // Check if the proposal has reached its vote threshold - if (proposal.YesVotes.Count >= proposal.VoteThreshold) - { - proposal.Status = "PASSED"; - } - else if (proposal.NoVotes.Count >= proposal.VoteThreshold) - { - proposal.Status = "DENIED"; - } + UpdateVoteCounts(proposal, Context.Sender, input.Vote, amount); + + return new Empty(); + } + + // Withdraws vote from a proposal. Can only be done after the proposal has ended. + public override Empty Withdraw(WithdrawInput input) + { + AssertProposalExists(input.ProposalId); + var proposal = GetProposal(input.ProposalId); + Assert(proposal.EndTimestamp <= Context.CurrentBlockTime, $"Proposal {proposal.Id} has not ended. Withdrawal is not allowed."); + + TransferTokensForProposalWithdrawal(proposal.Id); - return proposal; + return new Empty(); } // Returns all proposals in the DAO. @@ -102,7 +95,7 @@ public override ProposalList GetAllProposals(Empty input) // Create a new list called ProposalList var proposals = new ProposalList(); // Start iterating through Proposals from index 0 until the value of NextProposalId, read the corresponding proposal, add it to ProposalList, and finally return ProposalList - for (var i = 0; i < State.NextProposalId.Value; i++) + for (var i = StartProposalId; i < State.NextProposalId.Value; i++) { var proposalCount = i.ToString(); var proposal = State.Proposals[proposalCount]; @@ -114,22 +107,23 @@ public override ProposalList GetAllProposals(Empty input) // Get information of a particular proposal. public override Proposal GetProposal(StringValue input) { - var proposal = State.Proposals[input.Value]; - return proposal; + AssertProposalExists(input.Value); + + return GetProposal(input.Value); } - - // Get the number of members in the DAO - public override Int32Value GetMemberCount(Empty input) + + // Check if an address has voted + public override BoolValue HasVoted(HasVotedInput input) { - var memberCount = new Int32Value {Value = State.MemberCount.Value}; - return memberCount; - } + var id = input.ProposalId; + AssertProposalExists(id); - // Check if a member exists in the DAO - public override BoolValue GetMemberExist(Address input) + return new BoolValue { Value = State.Voters[id][input.Address] }; + } + + public override StringValue GetTokenSymbol(Empty input) { - var exist = new BoolValue {Value = State.Members[input]}; - return exist; + return new StringValue { Value = State.TokenSymbol.Value }; } } } \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/SimpleDAOState.cs b/templates/SimpleDAOContract/src/SimpleDAOState.cs index 9e0d120..40ed1eb 100644 --- a/templates/SimpleDAOContract/src/SimpleDAOState.cs +++ b/templates/SimpleDAOContract/src/SimpleDAOState.cs @@ -1,18 +1,15 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using AElf.Contracts.SimpleDAO; using AElf.Sdk.CSharp.State; using AElf.Types; namespace AElf.Contracts.SimpleDAO { // The state class is access the blockchain state - public class SimpleDAOState : ContractState + public partial class SimpleDAOState : ContractState { public BoolState Initialized { get; set; } - public MappedState Members { get; set; } public MappedState Proposals { get; set; } - public Int32State MemberCount { get; set; } + public MappedState Voters { get; set; } public Int32State NextProposalId { get; set; } + public StringState TokenSymbol { get; set; } } } \ No newline at end of file diff --git a/templates/SimpleDAOContract/src/SimpleDAO_Helper.cs b/templates/SimpleDAOContract/src/SimpleDAO_Helper.cs new file mode 100644 index 0000000..d8c82a4 --- /dev/null +++ b/templates/SimpleDAOContract/src/SimpleDAO_Helper.cs @@ -0,0 +1,99 @@ +using AElf.Contracts.MultiToken; +using AElf.Types; + +namespace AElf.Contracts.SimpleDAO +{ + public partial class SimpleDAO + { + private static Hash GetVirtualAddressHash(Address user, string proposalId) + { + return HashHelper.ConcatAndCompute(HashHelper.ComputeFrom(user), HashHelper.ComputeFrom(proposalId)); + } + + private Address GetVirtualAddress(Address user, string proposalId) + { + return Context.ConvertVirtualAddressToContractAddress(GetVirtualAddressHash(user, proposalId)); + } + + private Address GetVirtualAddress(Hash virtualAddressHash) + { + return Context.ConvertVirtualAddressToContractAddress(virtualAddressHash); + } + + private void TransferFrom(Address from, Address to, string symbol, long amount) + { + State.TokenContract.TransferFrom.Send( + new TransferFromInput + { + Symbol = symbol, + Amount = amount, + From = from, + Memo = "TransferIn", + To = to + }); + } + + private void TransferFromVirtualAddress(Hash virtualAddressHash, Address to, string symbol, long amount) + { + State.TokenContract.Transfer.VirtualSend(virtualAddressHash, + new TransferInput + { + Symbol = symbol, + Amount = amount, + Memo = "TransferOut", + To = to + }); + } + + private void AssertProposalExists(string proposalId) + { + var proposal = GetProposal(proposalId); + Assert(proposal != null, "Proposal not found."); + } + + private Proposal GetProposal(string proposalId) + { + return State.Proposals[proposalId]; + } + + private void TransferTokensForProposalBallot(string proposalId, long amount) + { + var virtualAddress = GetVirtualAddress(Context.Sender, proposalId); + TransferFrom(Context.Sender, virtualAddress, State.TokenSymbol.Value, amount); + } + + private void TransferTokensForProposalWithdrawal(string proposalId) + { + var virtualAddressHash = GetVirtualAddressHash(Context.Sender, proposalId); + + var output = State.TokenContract.GetBalance.VirtualCall(virtualAddressHash, new GetBalanceInput + { + Symbol = State.TokenSymbol.Value, + Owner = GetVirtualAddress(virtualAddressHash) + }); + + TransferFromVirtualAddress(virtualAddressHash, Context.Sender, State.TokenSymbol.Value, output.Balance); + } + + private void UpdateVoteCounts(Proposal proposal, Address voter, VoteOption voteOption, long amount) + { + switch (voteOption) + { + case VoteOption.Approved: + proposal.Result.ApproveCounts += amount; + break; + case VoteOption.Rejected: + proposal.Result.RejectCounts += amount; + break; + case VoteOption.Abstained: + proposal.Result.AbstainCounts += amount; + break; + default: + Assert(false, "Vote Option is invalid."); + break; + } + + State.Voters[proposal.Id][voter] = true; + } + } +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto b/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto index d4c551b..d5585f4 100644 --- a/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto +++ b/templates/SimpleDAOContract/test/Protobuf/stub/simple_dao.proto @@ -4,6 +4,7 @@ import "aelf/core.proto"; import "aelf/options.proto"; import "google/protobuf/empty.proto"; import "Protobuf/reference/acs12.proto"; +import public "google/protobuf/timestamp.proto"; // The namespace of this class option csharp_namespace = "AElf.Contracts.SimpleDAO"; @@ -16,22 +17,20 @@ service SimpleDAO { // Actions -> Methods that change state of smart contract // This method sets up the initial state of our StackUpDAO smart contract - rpc Initialize(google.protobuf.Empty) returns (google.protobuf.Empty); - - // This method allows a user to become a member of the DAO by taking in their - // address as an input parameter - rpc JoinDAO(aelf.Address) returns (google.protobuf.Empty); + rpc Initialize(InitializeInput) returns (google.protobuf.Empty); // This method allows a user to create a proposal for other users to vote on. // The method takes in a "CreateProposalInput" message which comprises of an // address, a title, description and a vote threshold (i.e how many votes // required for the proposal to pass) - rpc CreateProposal(CreateProposalInput) returns (Proposal); + rpc CreateProposal(CreateProposalInput) returns (google.protobuf.Empty); // This method allows a user to vote on proposals towards a specific proposal. // This method takes in a "VoteInput" message which takes in the address of // the voter, specific proposal and a boolean which represents their vote - rpc VoteOnProposal(VoteInput) returns (Proposal); + rpc Vote(VoteInput) returns (google.protobuf.Empty); + + rpc Withdraw(WithdrawInput) returns (google.protobuf.Empty); // Views -> Methods that does not change state of smart contract // This method allows a user to fetch a list of proposals that had been @@ -47,49 +46,65 @@ service SimpleDAO { option (aelf.is_view) = true; } - // This method allows a user to fetch the member count that joined DAO - rpc GetMemberCount (google.protobuf.Empty) returns (google.protobuf.Int32Value) { + // get the token symbol for this DAO + rpc GetTokenSymbol (google.protobuf.Empty) returns (google.protobuf.StringValue) { option (aelf.is_view) = true; } - // This method allows a user to check whether this member is exist by address - rpc GetMemberExist (aelf.Address) returns (google.protobuf.BoolValue) { + rpc HasVoted (HasVotedInput) returns (google.protobuf.BoolValue) { option (aelf.is_view) = true; } } -// Message definitions -message Member { - aelf.Address address = 1; -} - message Proposal { string id = 1; string title = 2; string description = 3; - repeated aelf.Address yesVotes = 4; - repeated aelf.Address noVotes = 5; - string status = 6; // e.g., "IN PROGRESS", "PASSED", "DENIED" - int32 voteThreshold = 7; + string status = 4; // e.g., "IN PROGRESS", "PASSED", "DENIED" + aelf.Address proposer = 5; + google.protobuf.Timestamp start_timestamp = 6; + google.protobuf.Timestamp end_timestamp = 7; + ProposalResult result = 8; +} + +message ProposalResult { + int64 approve_counts = 1; + int64 reject_counts = 2; + int64 abstain_counts = 3; } message CreateProposalInput { - aelf.Address creator = 1; - string title = 2; - string description = 3; - int32 voteThreshold = 4; + string title = 1; + string description = 2; + google.protobuf.Timestamp start_timestamp = 3; + google.protobuf.Timestamp end_timestamp = 4; +} + +enum VoteOption { + APPROVED = 0; + REJECTED = 1; + ABSTAINED = 2; } message VoteInput { - aelf.Address voter = 1; - string proposalId = 2; - bool vote = 3; // true for yes, false for no + string proposalId = 1; + VoteOption vote = 2; + int64 amount = 3; } -message MemberList { - repeated Member members = 1; +message InitializeInput { + string tokenSymbol = 1; +} + +message WithdrawInput { + string proposalId = 1; } message ProposalList { repeated Proposal proposals = 1; +} + +message HasVotedInput { + string proposalId = 1; + aelf.Address address = 2; } \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/Protobuf/stub/token_contract.proto b/templates/SimpleDAOContract/test/Protobuf/stub/token_contract.proto new file mode 100644 index 0000000..6d73c0b --- /dev/null +++ b/templates/SimpleDAOContract/test/Protobuf/stub/token_contract.proto @@ -0,0 +1,855 @@ +/** + * MultiToken contract. + */ +syntax = "proto3"; + +package token; + +import "aelf/core.proto"; +import "aelf/options.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; + +option csharp_namespace = "AElf.Contracts.MultiToken"; + +service TokenContract { + // Create a new token. + rpc Create (CreateInput) returns (google.protobuf.Empty) { + } + + // Issuing some amount of tokens to an address is the action of increasing that addresses balance + // for the given token. The total amount of issued tokens must not exceed the total supply of the token + // and only the issuer (creator) of the token can issue tokens. + // Issuing tokens effectively increases the circulating supply. + rpc Issue (IssueInput) returns (google.protobuf.Empty) { + } + + // Transferring tokens simply is the action of transferring a given amount of tokens from one address to another. + // The origin or source address is the signer of the transaction. + // The balance of the sender must be higher than the amount that is transferred. + rpc Transfer (TransferInput) returns (google.protobuf.Empty) { + } + + // The TransferFrom action will transfer a specified amount of tokens from one address to another. + // For this operation to succeed the from address needs to have approved (see allowances) enough tokens + // to Sender of this transaction. If successful the amount will be removed from the allowance. + rpc TransferFrom (TransferFromInput) returns (google.protobuf.Empty) { + } + + // The approve action increases the allowance from the Sender to the Spender address, + // enabling the Spender to call TransferFrom. + rpc Approve (ApproveInput) returns (google.protobuf.Empty) { + } + + // This is the reverse operation for Approve, it will decrease the allowance. + rpc UnApprove (UnApproveInput) returns (google.protobuf.Empty) { + } + + // This method can be used to lock tokens. + rpc Lock (LockInput) returns (google.protobuf.Empty) { + } + + // This is the reverse operation of locking, it un-locks some previously locked tokens. + rpc Unlock (UnlockInput) returns (google.protobuf.Empty) { + } + + // This action will burn the specified amount of tokens, removing them from the token’s Supply. + rpc Burn (BurnInput) returns (google.protobuf.Empty) { + } + + // Set the primary token of side chain. + rpc SetPrimaryTokenSymbol (SetPrimaryTokenSymbolInput) returns (google.protobuf.Empty) { + } + + // This interface is used for cross-chain transfer. + rpc CrossChainTransfer (CrossChainTransferInput) returns (google.protobuf.Empty) { + } + + // This method is used to receive cross-chain transfers. + rpc CrossChainReceiveToken (CrossChainReceiveTokenInput) returns (google.protobuf.Empty) { + } + + // The side chain creates tokens. + rpc CrossChainCreateToken(CrossChainCreateTokenInput) returns (google.protobuf.Empty) { + } + + // When the side chain is started, the side chain is initialized with the parent chain information. + rpc InitializeFromParentChain (InitializeFromParentChainInput) returns (google.protobuf.Empty) { + } + + // Handle the transaction fees charged by ChargeTransactionFees. + rpc ClaimTransactionFees (TotalTransactionFeesMap) returns (google.protobuf.Empty) { + } + + // Used to collect transaction fees. + rpc ChargeTransactionFees (ChargeTransactionFeesInput) returns (ChargeTransactionFeesOutput) { + } + + rpc ChargeUserContractTransactionFees(ChargeTransactionFeesInput) returns(ChargeTransactionFeesOutput){ + + } + + // Check the token threshold. + rpc CheckThreshold (CheckThresholdInput) returns (google.protobuf.Empty) { + } + + // Initialize coefficients of every type of tokens supporting charging fee. + rpc InitialCoefficients (google.protobuf.Empty) returns (google.protobuf.Empty){ + } + + // Processing resource token received. + rpc DonateResourceToken (TotalResourceTokensMaps) returns (google.protobuf.Empty) { + } + + // A transaction resource fee is charged to implement the ACS8 standards. + rpc ChargeResourceToken (ChargeResourceTokenInput) returns (google.protobuf.Empty) { + } + + // Verify that the resource token are sufficient. + rpc CheckResourceToken (google.protobuf.Empty) returns (google.protobuf.Empty) { + } + + // Set the list of tokens to pay transaction fees. + rpc SetSymbolsToPayTxSizeFee (SymbolListToPayTxSizeFee) returns (google.protobuf.Empty){ + } + + // Update the coefficient of the transaction fee calculation formula. + rpc UpdateCoefficientsForSender (UpdateCoefficientsInput) returns (google.protobuf.Empty) { + } + + // Update the coefficient of the transaction fee calculation formula. + rpc UpdateCoefficientsForContract (UpdateCoefficientsInput) returns (google.protobuf.Empty) { + } + + // This method is used to initialize the governance organization for some functions, + // including: the coefficient of the user transaction fee calculation formula, + // the coefficient of the contract developer resource fee calculation formula, and the side chain rental fee. + rpc InitializeAuthorizedController (google.protobuf.Empty) returns (google.protobuf.Empty){ + } + + rpc AddAddressToCreateTokenWhiteList (aelf.Address) returns (google.protobuf.Empty) { + } + rpc RemoveAddressFromCreateTokenWhiteList (aelf.Address) returns (google.protobuf.Empty) { + } + + rpc SetTransactionFeeDelegations (SetTransactionFeeDelegationsInput) returns (SetTransactionFeeDelegationsOutput){ + } + + rpc RemoveTransactionFeeDelegator (RemoveTransactionFeeDelegatorInput) returns (google.protobuf.Empty){ + } + + rpc RemoveTransactionFeeDelegatee (RemoveTransactionFeeDelegateeInput) returns (google.protobuf.Empty){ + } + + // Get all delegatees' address of delegator from input + rpc GetTransactionFeeDelegatees (GetTransactionFeeDelegateesInput) returns (GetTransactionFeeDelegateesOutput) { + option (aelf.is_view) = true; + } + + // Query token information. + rpc GetTokenInfo (GetTokenInfoInput) returns (TokenInfo) { + option (aelf.is_view) = true; + } + + // Query native token information. + rpc GetNativeTokenInfo (google.protobuf.Empty) returns (TokenInfo) { + option (aelf.is_view) = true; + } + + // Query resource token information. + rpc GetResourceTokenInfo (google.protobuf.Empty) returns (TokenInfoList) { + option (aelf.is_view) = true; + } + + // Query the balance at the specified address. + rpc GetBalance (GetBalanceInput) returns (GetBalanceOutput) { + option (aelf.is_view) = true; + } + + // Query the account's allowance for other addresses + rpc GetAllowance (GetAllowanceInput) returns (GetAllowanceOutput) { + option (aelf.is_view) = true; + } + + // Check whether the token is in the whitelist of an address, + // which can be called TransferFrom to transfer the token under the condition of not being credited. + rpc IsInWhiteList (IsInWhiteListInput) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } + + // Query the information for a lock. + rpc GetLockedAmount (GetLockedAmountInput) returns (GetLockedAmountOutput) { + option (aelf.is_view) = true; + } + + // Query the address of receiving token in cross-chain transfer. + rpc GetCrossChainTransferTokenContractAddress (GetCrossChainTransferTokenContractAddressInput) returns (aelf.Address) { + option (aelf.is_view) = true; + } + + // Query the name of the primary Token. + rpc GetPrimaryTokenSymbol (google.protobuf.Empty) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + // Query the coefficient of the transaction fee calculation formula. + rpc GetCalculateFeeCoefficientsForContract (google.protobuf.Int32Value) returns (CalculateFeeCoefficients) { + option (aelf.is_view) = true; + } + + // Query the coefficient of the transaction fee calculation formula. + rpc GetCalculateFeeCoefficientsForSender (google.protobuf.Empty) returns (CalculateFeeCoefficients) { + option (aelf.is_view) = true; + } + + // Query tokens that can pay transaction fees. + rpc GetSymbolsToPayTxSizeFee (google.protobuf.Empty) returns (SymbolListToPayTxSizeFee){ + option (aelf.is_view) = true; + } + + // Query the hash of the last input of ClaimTransactionFees. + rpc GetLatestTotalTransactionFeesMapHash (google.protobuf.Empty) returns (aelf.Hash){ + option (aelf.is_view) = true; + } + + // Query the hash of the last input of DonateResourceToken. + rpc GetLatestTotalResourceTokensMapsHash (google.protobuf.Empty) returns (aelf.Hash){ + option (aelf.is_view) = true; + } + rpc IsTokenAvailableForMethodFee (google.protobuf.StringValue) returns (google.protobuf.BoolValue) { + option (aelf.is_view) = true; + } + rpc GetReservedExternalInfoKeyList (google.protobuf.Empty) returns (StringList) { + option (aelf.is_view) = true; + } + + rpc GetTransactionFeeDelegationsOfADelegatee(GetTransactionFeeDelegationsOfADelegateeInput) returns(TransactionFeeDelegations){ + option (aelf.is_view) = true; + } +} + +message TokenInfo { + // The symbol of the token.f + string symbol = 1; + // The full name of the token. + string token_name = 2; + // The current supply of the token. + int64 supply = 3; + // The total supply of the token. + int64 total_supply = 4; + // The precision of the token. + int32 decimals = 5; + // The address that has permission to issue the token. + aelf.Address issuer = 6; + // A flag indicating if this token is burnable. + bool is_burnable = 7; + // The chain id of the token. + int32 issue_chain_id = 8; + // The amount of issued tokens. + int64 issued = 9; + // The external information of the token. + ExternalInfo external_info = 10; + // The address that owns the token. + aelf.Address owner = 11; +} + +message ExternalInfo { + map value = 1; +} + +message CreateInput { + // The symbol of the token. + string symbol = 1; + // The full name of the token. + string token_name = 2; + // The total supply of the token. + int64 total_supply = 3; + // The precision of the token + int32 decimals = 4; + // The address that has permission to issue the token. + aelf.Address issuer = 5; + // A flag indicating if this token is burnable. + bool is_burnable = 6; + // A whitelist address list used to lock tokens. + repeated aelf.Address lock_white_list = 7; + // The chain id of the token. + int32 issue_chain_id = 8; + // The external information of the token. + ExternalInfo external_info = 9; + // The address that owns the token. + aelf.Address owner = 10; +} + +message SetPrimaryTokenSymbolInput { + // The symbol of the token. + string symbol = 1; +} + +message IssueInput { + // The token symbol to issue. + string symbol = 1; + // The token amount to issue. + int64 amount = 2; + // The memo. + string memo = 3; + // The target address to issue. + aelf.Address to = 4; +} + +message TransferInput { + // The receiver of the token. + aelf.Address to = 1; + // The token symbol to transfer. + string symbol = 2; + // The amount to to transfer. + int64 amount = 3; + // The memo. + string memo = 4; +} + +message LockInput { + // The one want to lock his token. + aelf.Address address = 1; + // Id of the lock. + aelf.Hash lock_id = 2; + // The symbol of the token to lock. + string symbol = 3; + // a memo. + string usage = 4; + // The amount of tokens to lock. + int64 amount = 5; +} + +message UnlockInput { + // The one want to un-lock his token. + aelf.Address address = 1; + // Id of the lock. + aelf.Hash lock_id = 2; + // The symbol of the token to un-lock. + string symbol = 3; + // a memo. + string usage = 4; + // The amount of tokens to un-lock. + int64 amount = 5; +} + +message TransferFromInput { + // The source address of the token. + aelf.Address from = 1; + // The destination address of the token. + aelf.Address to = 2; + // The symbol of the token to transfer. + string symbol = 3; + // The amount to transfer. + int64 amount = 4; + // The memo. + string memo = 5; +} + +message ApproveInput { + // The address that allowance will be increased. + aelf.Address spender = 1; + // The symbol of token to approve. + string symbol = 2; + // The amount of token to approve. + int64 amount = 3; +} + +message UnApproveInput { + // The address that allowance will be decreased. + aelf.Address spender = 1; + // The symbol of token to un-approve. + string symbol = 2; + // The amount of token to un-approve. + int64 amount = 3; +} + +message BurnInput { + // The symbol of token to burn. + string symbol = 1; + // The amount of token to burn. + int64 amount = 2; +} + +message ChargeResourceTokenInput { + // Collection of charge resource token, Symbol->Amount. + map cost_dic = 1; + // The sender of the transaction. + aelf.Address caller = 2; +} + +message TransactionFeeBill { + // The transaction fee dictionary, Symbol->fee. + map fees_map = 1; +} + +message TransactionFreeFeeAllowanceBill { + // The transaction free fee allowance dictionary, Symbol->fee. + map free_fee_allowances_map = 1; +} + +message CheckThresholdInput { + // The sender of the transaction. + aelf.Address sender = 1; + // The threshold to set, Symbol->Threshold. + map symbol_to_threshold = 2; + // Whether to check the allowance. + bool is_check_allowance = 3; +} + +message GetTokenInfoInput { + // The symbol of token. + string symbol = 1; +} + +message GetBalanceInput { + // The symbol of token. + string symbol = 1; + // The target address of the query. + aelf.Address owner = 2; +} + +message GetBalanceOutput { + // The symbol of token. + string symbol = 1; + // The target address of the query. + aelf.Address owner = 2; + // The balance of the owner. + int64 balance = 3; +} + +message GetAllowanceInput { + // The symbol of token. + string symbol = 1; + // The address of the token owner. + aelf.Address owner = 2; + // The address of the spender. + aelf.Address spender = 3; +} + +message GetAllowanceOutput { + // The symbol of token. + string symbol = 1; + // The address of the token owner. + aelf.Address owner = 2; + // The address of the spender. + aelf.Address spender = 3; + // The amount of allowance. + int64 allowance = 4; +} + +message CrossChainTransferInput { + // The receiver of transfer. + aelf.Address to = 1; + // The symbol of token. + string symbol = 2; + // The amount of token to transfer. + int64 amount = 3; + // The memo. + string memo = 4; + // The destination chain id. + int32 to_chain_id = 5; + // The chain id of the token. + int32 issue_chain_id = 6; +} + +message CrossChainReceiveTokenInput { + // The source chain id. + int32 from_chain_id = 1; + // The height of the transfer transaction. + int64 parent_chain_height = 2; + // The raw bytes of the transfer transaction. + bytes transfer_transaction_bytes = 3; + // The merkle path created from the transfer transaction. + aelf.MerklePath merkle_path = 4; +} + +message IsInWhiteListInput { + // The symbol of token. + string symbol = 1; + // The address to check. + aelf.Address address = 2; +} + +message SymbolToPayTxSizeFee{ + // The symbol of token. + string token_symbol = 1; + // The charge weight of primary token. + int32 base_token_weight = 2; + // The new added token charge weight. For example, the charge weight of primary Token is set to 1. + // The newly added token charge weight is set to 10. If the transaction requires 1 unit of primary token, + // the user can also pay for 10 newly added tokens. + int32 added_token_weight = 3; +} + +message SymbolListToPayTxSizeFee{ + // Transaction fee token information. + repeated SymbolToPayTxSizeFee symbols_to_pay_tx_size_fee = 1; +} + +message ChargeTransactionFeesInput { + // The method name of transaction. + string method_name = 1; + // The contract address of transaction. + aelf.Address contract_address = 2; + // The amount of transaction size fee. + int64 transaction_size_fee = 3; + // Transaction fee token information. + repeated SymbolToPayTxSizeFee symbols_to_pay_tx_size_fee = 4; +} + +message ChargeTransactionFeesOutput { + // Whether the charge was successful. + bool success = 1; + // The charging information. + string charging_information = 2; +} + +message CallbackInfo { + aelf.Address contract_address = 1; + string method_name = 2; +} + +message ExtraTokenListModified { + option (aelf.is_event) = true; + // Transaction fee token information. + SymbolListToPayTxSizeFee symbol_list_to_pay_tx_size_fee = 1; +} + +message GetLockedAmountInput { + // The address of the lock. + aelf.Address address = 1; + // The token symbol. + string symbol = 2; + // The id of the lock. + aelf.Hash lock_id = 3; +} + +message GetLockedAmountOutput { + // The address of the lock. + aelf.Address address = 1; + // The token symbol. + string symbol = 2; + // The id of the lock. + aelf.Hash lock_id = 3; + // The locked amount. + int64 amount = 4; +} + +message TokenInfoList { + // List of token information. + repeated TokenInfo value = 1; +} + +message GetCrossChainTransferTokenContractAddressInput { + // The chain id. + int32 chainId = 1; +} + +message CrossChainCreateTokenInput { + // The chain id of the chain on which the token was created. + int32 from_chain_id = 1; + // The height of the transaction that created the token. + int64 parent_chain_height = 2; + // The transaction that created the token. + bytes transaction_bytes = 3; + // The merkle path created from the transaction that created the transaction. + aelf.MerklePath merkle_path = 4; +} + +message InitializeFromParentChainInput { + // The amount of resource. + map resource_amount = 1; + // The token contract addresses. + map registered_other_token_contract_addresses = 2; + // The creator the side chain. + aelf.Address creator = 3; +} + +message UpdateCoefficientsInput { + // The specify pieces gonna update. + repeated int32 piece_numbers = 1; + // Coefficients of one single type. + CalculateFeeCoefficients coefficients = 2; +} + +enum FeeTypeEnum { + READ = 0; + STORAGE = 1; + WRITE = 2; + TRAFFIC = 3; + TX = 4; +} + +message CalculateFeePieceCoefficients { + // Coefficients of one single piece. + // The first char is its type: liner / power. + // The second char is its piece upper bound. + repeated int32 value = 1; +} + +message CalculateFeeCoefficients { + // The resource fee type, like READ, WRITE, etc. + int32 fee_token_type = 1; + // Coefficients of one single piece. + repeated CalculateFeePieceCoefficients piece_coefficients_list = 2; +} + +message AllCalculateFeeCoefficients { + // The coefficients of fee Calculation. + repeated CalculateFeeCoefficients value = 1; +} + +message TotalTransactionFeesMap +{ + // Token dictionary that charge transaction fee, Symbol->Amount. + map value = 1; + // The hash of the block processing the transaction. + aelf.Hash block_hash = 2; + // The height of the block processing the transaction. + int64 block_height = 3; +} + +message TotalResourceTokensMaps { + // Resource tokens to charge. + repeated ContractTotalResourceTokens value = 1; + // The hash of the block processing the transaction. + aelf.Hash block_hash = 2; + // The height of the block processing the transaction. + int64 block_height = 3; +} + +message ContractTotalResourceTokens { + // The contract address. + aelf.Address contract_address = 1; + // Resource tokens to charge. + TotalResourceTokensMap tokens_map = 2; +} + +message TotalResourceTokensMap +{ + // Resource token dictionary, Symbol->Amount. + map value = 1; +} + +message StringList { + repeated string value = 1; +} + +message TransactionFeeDelegations{ + // delegation, symbols and its' amount + map delegations = 1; + // height when added + int64 block_height = 2; + //Whether to pay transaction fee continuously + bool isUnlimitedDelegate = 3; +} + +message TransactionFeeDelegatees{ + map delegatees = 1; +} + +message SetTransactionFeeDelegationsInput { + // the delegator address + aelf.Address delegator_address = 1; + // delegation, symbols and its' amount + map delegations = 2; +} + +message SetTransactionFeeDelegationsOutput { + bool success = 1; +} + +message RemoveTransactionFeeDelegatorInput{ + // the delegator address + aelf.Address delegator_address = 1; +} + +message RemoveTransactionFeeDelegateeInput { + // the delegatee address + aelf.Address delegatee_address = 1; +} + +message GetTransactionFeeDelegationsOfADelegateeInput { + aelf.Address delegatee_address = 1; + aelf.Address delegator_address = 2; +} + +message GetTransactionFeeDelegateesInput { + aelf.Address delegator_address = 1; +} + +message GetTransactionFeeDelegateesOutput { + repeated aelf.Address delegatee_addresses = 1; +} + +// Events + +message Transferred { + option (aelf.is_event) = true; + // The source address of the transferred token. + aelf.Address from = 1 [(aelf.is_indexed) = true]; + // The destination address of the transferred token. + aelf.Address to = 2 [(aelf.is_indexed) = true]; + // The symbol of the transferred token. + string symbol = 3 [(aelf.is_indexed) = true]; + // The amount of the transferred token. + int64 amount = 4; + // The memo. + string memo = 5; +} + +message Approved { + option (aelf.is_event) = true; + // The address of the token owner. + aelf.Address owner = 1 [(aelf.is_indexed) = true]; + // The address that allowance be increased. + aelf.Address spender = 2 [(aelf.is_indexed) = true]; + // The symbol of approved token. + string symbol = 3 [(aelf.is_indexed) = true]; + // The amount of approved token. + int64 amount = 4; +} + +message UnApproved { + option (aelf.is_event) = true; + // The address of the token owner. + aelf.Address owner = 1 [(aelf.is_indexed) = true]; + // The address that allowance be decreased. + aelf.Address spender = 2 [(aelf.is_indexed) = true]; + // The symbol of un-approved token. + string symbol = 3 [(aelf.is_indexed) = true]; + // The amount of un-approved token. + int64 amount = 4; +} + +message Burned +{ + option (aelf.is_event) = true; + // The address who wants to burn token. + aelf.Address burner = 1 [(aelf.is_indexed) = true]; + // The symbol of burned token. + string symbol = 2 [(aelf.is_indexed) = true]; + // The amount of burned token. + int64 amount = 3; +} + +message ChainPrimaryTokenSymbolSet { + option (aelf.is_event) = true; + // The symbol of token. + string token_symbol = 1; +} + +message CalculateFeeAlgorithmUpdated { + option (aelf.is_event) = true; + // All calculate fee coefficients after modification. + AllCalculateFeeCoefficients all_type_fee_coefficients = 1; +} + +message RentalCharged { + option (aelf.is_event) = true; + // The symbol of rental fee charged. + string symbol = 1; + // The amount of rental fee charged. + int64 amount = 2; + // The payer of rental fee. + aelf.Address payer = 3; + // The receiver of rental fee. + aelf.Address receiver = 4; +} + +message RentalAccountBalanceInsufficient { + option (aelf.is_event) = true; + // The symbol of insufficient rental account balance. + string symbol = 1; + // The balance of the account. + int64 amount = 2; +} + +message TokenCreated { + option (aelf.is_event) = true; + // The symbol of the token. + string symbol = 1; + // The full name of the token. + string token_name = 2; + // The total supply of the token. + int64 total_supply = 3; + // The precision of the token. + int32 decimals = 4; + // The address that has permission to issue the token. + aelf.Address issuer = 5; + // A flag indicating if this token is burnable. + bool is_burnable = 6; + // The chain id of the token. + int32 issue_chain_id = 7; + // The external information of the token. + ExternalInfo external_info = 8; + // The address that owns the token. + aelf.Address owner = 9; +} + +message Issued { + option (aelf.is_event) = true; + // The symbol of issued token. + string symbol = 1; + // The amount of issued token. + int64 amount = 2; + // The memo. + string memo = 3; + // The issued target address. + aelf.Address to = 4; +} + +message CrossChainTransferred { + option (aelf.is_event) = true; + // The source address of the transferred token. + aelf.Address from = 1; + // The destination address of the transferred token. + aelf.Address to = 2; + // The symbol of the transferred token. + string symbol = 3; + // The amount of the transferred token. + int64 amount = 4; + // The memo. + string memo = 5; + // The destination chain id. + int32 to_chain_id = 6; + // The chain id of the token. + int32 issue_chain_id = 7; +} + +message CrossChainReceived { + option (aelf.is_event) = true; + // The source address of the transferred token. + aelf.Address from = 1; + // The destination address of the transferred token. + aelf.Address to = 2; + // The symbol of the received token. + string symbol = 3; + // The amount of the received token. + int64 amount = 4; + // The memo. + string memo = 5; + // The destination chain id. + int32 from_chain_id = 6; + // The chain id of the token. + int32 issue_chain_id = 7; + // The parent chain height of the transfer transaction. + int64 parent_chain_height = 8; + // The id of transfer transaction. + aelf.Hash transfer_transaction_id =9; +} + +message TransactionFeeDelegationAdded { + option (aelf.is_event) = true; + aelf.Address delegator = 1 [(aelf.is_indexed) = true]; + aelf.Address delegatee = 2 [(aelf.is_indexed) = true]; + aelf.Address caller = 3 [(aelf.is_indexed) = true]; +} + +message TransactionFeeDelegationCancelled { + option (aelf.is_event) = true; + aelf.Address delegator = 1 [(aelf.is_indexed) = true]; + aelf.Address delegatee = 2 [(aelf.is_indexed) = true]; + aelf.Address caller = 3 [(aelf.is_indexed) = true]; +} \ No newline at end of file diff --git a/templates/SimpleDAOContract/test/SimpleDAOTests.cs b/templates/SimpleDAOContract/test/SimpleDAOTests.cs index 44fd627..d2696c0 100644 --- a/templates/SimpleDAOContract/test/SimpleDAOTests.cs +++ b/templates/SimpleDAOContract/test/SimpleDAOTests.cs @@ -1,5 +1,9 @@ using System; +using System.Linq; using System.Threading.Tasks; +using AElf.Contracts.MultiToken; +using AElf.CSharp.Core; +using AElf.CSharp.Core.Extension; using AElf.Types; using Google.Protobuf.WellKnownTypes; using Shouldly; @@ -10,131 +14,861 @@ namespace AElf.Contracts.SimpleDAO // This class is unit test class, and it inherit TestBase. Write your unit test code inside it public class SimpleDAOTests : TestBase { + private const string TokenSymbol = "ELF"; + private const long BallotAmount = 5; + private const int DefaultProposalEndTimeOffset = 100; + [Fact] - public async Task InitializeTest_Success() + public async Task InitializeContract_Success() { - await SimpleDAOStub.Initialize.SendAsync(new Empty()); - var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue {Value = "0"}); - proposal.Title.ShouldBe("Proposal #1"); + // Act + var result = await InitializeSimpleDaoContract(); + + // Assert + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var symbol = await SimpleDAOStub.GetTokenSymbol.CallAsync(new Empty()); + symbol.Value.ShouldBe(TokenSymbol); } [Fact] - public async Task InitializeTest_Duplicate() + public Task InitializeContract_Fail_TokenSymbolDoesNotExist() { - await SimpleDAOStub.Initialize.SendAsync(new Empty()); - var executionResult = await SimpleDAOStub.Initialize.SendWithExceptionAsync(new Empty()); - executionResult.TransactionResult.Error.ShouldContain("already initialized"); + // Act + var invalidInput = new InitializeInput + { + TokenSymbol = "MOCK_TOKEN_SYMBOL" + }; + + // Act & Assert + Should.Throw(async () => await SimpleDAOStub.Initialize.SendAsync(invalidInput)); + return Task.CompletedTask; + } + + [Fact] + public async Task InitializeContract_Fail_AlreadyInitialized() + { + // Arrange + await InitializeSimpleDaoContract(); + + // Act & Assert + Should.Throw(async () => await SimpleDAOStub.Initialize.SendAsync(new InitializeInput + { + TokenSymbol = TokenSymbol + })); } [Fact] - public async Task JoinDAOTest_Success() + public async Task CreateProposal_Success() { - await SimpleDAOStub.Initialize.SendAsync(new Empty()); - await SimpleDAOStub.JoinDAO.SendAsync(Accounts[1].Address); - await SimpleDAOStub.JoinDAO.SendAsync(Accounts[2].Address); - var exist1 = await SimpleDAOStub.GetMemberExist.CallAsync(Accounts[1].Address); - var exist2 = await SimpleDAOStub.GetMemberExist.CallAsync(Accounts[2].Address); - exist1.Value.ShouldBe(true); - exist2.Value.ShouldBe(true); + await InitializeSimpleDaoContract(); + + var input = new CreateProposalInput + { + Title = "Test Proposal", + Description = "This is a test proposal.", + EndTimestamp = Timestamp.FromDateTime(DateTime.UtcNow.AddSeconds(DefaultProposalEndTimeOffset)), + StartTimestamp = Timestamp.FromDateTime(DateTime.UtcNow) + }; + + var result = await SimpleDAOStub.CreateProposal.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + const string proposalId = "1"; + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue{ Value = proposalId }); + proposal.ShouldNotBeNull(); + proposal.Title.ShouldBe(input.Title); + proposal.Description.ShouldBe(input.Description); + } + + [Fact] + public async Task CreateProposal_EmptyTitle_ShouldThrow() + { + await InitializeSimpleDaoContract(); + + var input = new CreateProposalInput + { + Title = "", + Description = "This is a test proposal.", + EndTimestamp = Timestamp.FromDateTime(DateTime.UtcNow.AddSeconds(DefaultProposalEndTimeOffset)), + StartTimestamp = Timestamp.FromDateTime(DateTime.UtcNow) + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.CreateProposal.SendAsync(input)); + exception.Message.ShouldContain("Title should not be empty."); + } + + [Fact] + public async Task CreateProposal_EmptyDescription_ShouldThrow() + { + await InitializeSimpleDaoContract(); + + var input = new CreateProposalInput + { + Title = "Mock Proposal", + Description = "", + EndTimestamp = Timestamp.FromDateTime(DateTime.UtcNow.AddSeconds(DefaultProposalEndTimeOffset)), + StartTimestamp = Timestamp.FromDateTime(DateTime.UtcNow) + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.CreateProposal.SendAsync(input)); + exception.Message.ShouldContain("Description should not be empty."); } [Fact] - public async Task JoinDAOTest_Duplicate() + public async Task CreateProposal_InvalidStartTime_ShouldThrow() { - await SimpleDAOStub.Initialize.SendAsync(new Empty()); - await SimpleDAOStub.JoinDAO.SendAsync(Accounts[1].Address); - var executionResult = await SimpleDAOStub.JoinDAO.SendWithExceptionAsync(Accounts[1].Address); - executionResult.TransactionResult.Error.ShouldContain("Member is already in the DAO"); + await InitializeSimpleDaoContract(); + + var input = new CreateProposalInput + { + Title = "Mock Proposal", + Description = "This is a test proposal.", + EndTimestamp = Timestamp.FromDateTime(DateTime.UtcNow.AddSeconds(DefaultProposalEndTimeOffset)), + StartTimestamp = Timestamp.FromDateTime(DateTime.UtcNow.AddSeconds(-1)) + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.CreateProposal.SendAsync(input)); + exception.Message.ShouldContain("Start time should be greater or equal to current block time."); } [Fact] - public async Task CreateProposalTest_Success() + public async Task CreateProposal_InvalidExpireTime_ShouldThrow() + { + await InitializeSimpleDaoContract(); + + var input = new CreateProposalInput + { + Title = "Mock Proposal", + Description = "This is a test proposal.", + EndTimestamp = Timestamp.FromDateTime(DateTime.UtcNow.AddSeconds(-100)), + StartTimestamp = Timestamp.FromDateTime(DateTime.UtcNow) + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.CreateProposal.SendAsync(input)); + exception.Message.ShouldContain("Expire time should be greater than current block time."); + } + + [Fact] + public async Task Vote_Success_Approve() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + var result = await SimpleDAOStub.Vote.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + proposal.Result.ApproveCounts.ShouldBe(BallotAmount); + proposal.Result.RejectCounts.ShouldBe(0); + proposal.Result.AbstainCounts.ShouldBe(0); + } + + [Fact] + public async Task Vote_Success_Reject() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Rejected, + Amount = BallotAmount + }; + + var result = await SimpleDAOStub.Vote.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + proposal.Result.ApproveCounts.ShouldBe(0); + proposal.Result.RejectCounts.ShouldBe(BallotAmount); + proposal.Result.AbstainCounts.ShouldBe(0); + } + + [Fact] + public async Task Vote_Success_Abstain() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Abstained, + Amount = BallotAmount + }; + + var result = await SimpleDAOStub.Vote.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + proposal.Result.ApproveCounts.ShouldBe(0); + proposal.Result.RejectCounts.ShouldBe(0); + proposal.Result.AbstainCounts.ShouldBe(BallotAmount); + } + + [Fact] + public async Task Vote_ProposalNotFound_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var input = new VoteInput + { + ProposalId = "1", + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Vote.SendAsync(input)); + exception.Message.ShouldContain("Proposal not found."); + } + + [Fact] + public async Task Vote_ProposalNotStarted_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var now = BlockTimeProvider.GetBlockTime(); + var proposalId = await CreateTestProposalAsync(now.AddSeconds(DefaultProposalEndTimeOffset), now.AddSeconds(200)); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + BlockTimeProvider.SetBlockTime(3600 * 24 * 8 * 1000); + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Vote.SendAsync(input)); + exception.Message.ShouldContain($"Proposal {proposalId} has ended. Voting is not allowed."); + } + + [Fact] + public async Task Vote_ExpiredProposal_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + BlockTimeProvider.SetBlockTime(3600 * 24 * 8 * 1000); + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Vote.SendAsync(input)); + exception.Message.ShouldContain($"Proposal {proposalId} has ended. Voting is not allowed."); + } + + [Fact] + public async Task Vote_AlreadyVoted_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + await SimpleDAOStub.Vote.SendAsync(input); + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Vote.SendAsync(input)); + exception.Message.ShouldContain("You have already voted."); + } + + [Fact] + public async Task Vote_MultipleUsers_ShouldSucceed() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + var user1Stub = GetTester(ContractAddress, Accounts[1].KeyPair); + var user1TokenStub = GetTester(TokenContractAddress, Accounts[1].KeyPair); + await TokenContractApprove(user1TokenStub); + await SendTokenTo(Accounts[1].Address); + + var user2Stub = GetTester(ContractAddress, Accounts[2].KeyPair); + var user2TokenStub = GetTester(TokenContractAddress, Accounts[2].KeyPair); + await TokenContractApprove(user2TokenStub); + await SendTokenTo(Accounts[2].Address); + + await user1Stub.Vote.SendAsync(input); + await user2Stub.Vote.SendAsync(input); + + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + proposal.Result.ApproveCounts.ShouldBe(BallotAmount * 2); + proposal.Result.RejectCounts.ShouldBe(0); + proposal.Result.AbstainCounts.ShouldBe(0); + } + + [Fact] + public async Task Vote_MultipleVotes_ShouldAccumulate() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var inputAgree = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + var inputDisagree = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Rejected, + Amount = BallotAmount + }; + + var user1Stub = GetTester(ContractAddress, Accounts[1].KeyPair); + var user1TokenStub = GetTester(TokenContractAddress, Accounts[1].KeyPair); + await TokenContractApprove(user1TokenStub); + await SendTokenTo(Accounts[1].Address); + + var user2Stub = GetTester(ContractAddress, Accounts[2].KeyPair); + var user2TokenStub = GetTester(TokenContractAddress, Accounts[2].KeyPair); + await TokenContractApprove(user2TokenStub); + await SendTokenTo(Accounts[2].Address); + + await user1Stub.Vote.SendAsync(inputAgree); + await user2Stub.Vote.SendAsync(inputDisagree); + + var proposal = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + proposal.Result.ApproveCounts.ShouldBe(BallotAmount); + proposal.Result.RejectCounts.ShouldBe(BallotAmount); + proposal.Result.AbstainCounts.ShouldBe(0); + } + + [Fact] + public async Task Withdraw_Success() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var voteInput = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + await SimpleDAOStub.Vote.SendAsync(voteInput); + + var initialBalance = TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = Accounts[0].Address, + Symbol = TokenSymbol + }).Result.Balance; + + // fast forward to proposal end time + BlockTimeProvider.SetBlockTime(DefaultProposalEndTimeOffset * 1000); + + var withdrawInput = new WithdrawInput + { + ProposalId = proposalId + }; + + var result = await SimpleDAOStub.Withdraw.SendAsync(withdrawInput); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var newBalance = TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = Accounts[0].Address, + Symbol = TokenSymbol + }).Result.Balance; + + newBalance.ShouldBe(initialBalance + BallotAmount); + } + + [Fact] + public async Task Withdraw_ProposalNotFound_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var input = new WithdrawInput + { + ProposalId = "1" + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Withdraw.SendAsync(input)); + exception.Message.ShouldContain("Proposal not found."); + } + + [Fact] + public async Task Withdraw_NotVoter_ShouldThrow() { - await JoinDAOTest_Success(); - var proposal = await CreateMockProposal(Accounts[1].Address); - proposal.Title.ShouldBe("mock_proposal"); - proposal.Id.ShouldBe("1"); + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new WithdrawInput + { + ProposalId = proposalId + }; + + // fast forward to proposal end time + BlockTimeProvider.SetBlockTime(DefaultProposalEndTimeOffset * 1000); + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Withdraw.SendAsync(input)); + exception.Message.ShouldContain("Invalid amount."); } - private async Task CreateMockProposal(Address creator) + [Fact] + public async Task Withdraw_BeforeExpiry_ShouldThrow() { - var createProposalInput = new CreateProposalInput + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new WithdrawInput { - Creator = creator, - Description = "mock_proposal_desc", - Title = "mock_proposal", - VoteThreshold = 1 + ProposalId = proposalId }; - var proposal = await SimpleDAOStub.CreateProposal.SendAsync(createProposalInput); - return proposal.Output; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.Withdraw.SendAsync(input)); + exception.Message.ShouldContain($"Proposal {proposalId} has not ended. Withdrawal is not allowed."); } [Fact] - public async Task CreateProposalTest_NoPermission() + public async Task Withdraw_MultipleUsersAttempting_Success() { - await JoinDAOTest_Success(); + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new WithdrawInput + { + ProposalId = proposalId + }; + var inputAgree = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + + var user1Stub = GetTester(ContractAddress, Accounts[1].KeyPair); + var user1TokenStub = GetTester(TokenContractAddress, Accounts[1].KeyPair); + await TokenContractApprove(user1TokenStub); + await SendTokenTo(Accounts[1].Address); + + var user2Stub = GetTester(ContractAddress, Accounts[2].KeyPair); + var user2TokenStub = GetTester(TokenContractAddress, Accounts[2].KeyPair); + await TokenContractApprove(user2TokenStub); + await SendTokenTo(Accounts[2].Address); + + await user1Stub.Vote.SendAsync(inputAgree); + await user2Stub.Vote.SendAsync(inputAgree); + + // fast forward to proposal end time + BlockTimeProvider.SetBlockTime(DefaultProposalEndTimeOffset * 1000); + + // Ensure the multiple voters can still withdraw + var result = await user1Stub.Withdraw.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + result = await user2Stub.Withdraw.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + } + + [Fact] + public async Task GetAllProposals_Empty() + { + var result = await SimpleDAOStub.GetAllProposals.CallAsync(new Empty()); + result.Proposals.ShouldNotBeNull(); + result.Proposals.Count.ShouldBe(0); + } + + [Fact] + public async Task GetAllProposals_OneProposal() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + + var result = await SimpleDAOStub.GetAllProposals.CallAsync(new Empty()); + result.Proposals.ShouldNotBeNull(); + result.Proposals.Count.ShouldBe(1); + + var proposal = result.Proposals.First(); + proposal.Id.ShouldBe(proposalId); + proposal.Title.ShouldBe("Test Proposal"); + proposal.Description.ShouldBe("This is a test proposal."); + } + + [Fact] + public async Task GetAllProposals_MultipleProposals() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId1 = await CreateTestProposalAsync(); + var proposalId2 = await CreateTestProposalAsync(); + + var result = await SimpleDAOStub.GetAllProposals.CallAsync(new Empty()); + result.Proposals.ShouldNotBeNull(); + result.Proposals.Count.ShouldBe(2); + + //catch exception if predicate is not met + Proposal proposal1 = null; + Proposal proposal2 = null; try { - await CreateMockProposal(Accounts[2].Address); + proposal1 = result.Proposals.First(p => p.Id == proposalId1); + proposal2 = result.Proposals.First(p => p.Id == proposalId2); } - catch (Exception e) + catch (InvalidOperationException) { - e.Message.ShouldContain("Only DAO members can create proposals"); + Assert.False(true, "Proposal not found."); } + + proposal1.ShouldNotBeNull(); + proposal2.ShouldNotBeNull(); } - + [Fact] - public async Task VoteOnProposalTest_Success() + public async Task GetAllProposals_ProposalsWithDifferentStates() { - await JoinDAOTest_Success(); - var proposal = await CreateMockProposal(Accounts[1].Address); - var voteInput1 = new VoteInput + await InitializeAndApproveSimpleDaoContract(); + + var proposalId1 = await CreateTestProposalAsync(); + var proposalId2 = await CreateTestProposalAsync(); + + // Simulate voting to change state of proposals + var voteInput = new VoteInput { - ProposalId = proposal.Id, - Vote = true, - Voter = Accounts[1].Address + ProposalId = proposalId1, + Vote = VoteOption.Approved, + Amount = BallotAmount }; - var voteInput2 = new VoteInput + await SimpleDAOStub.Vote.SendAsync(voteInput); + + var result = await SimpleDAOStub.GetAllProposals.CallAsync(new Empty()); + result.Proposals.ShouldNotBeNull(); + result.Proposals.Count.ShouldBe(2); + + var proposal1 = result.Proposals.First(p => p.Id == proposalId1); + var proposal2 = result.Proposals.First(p => p.Id == proposalId2); + + proposal1.Result.ApproveCounts.ShouldBe(BallotAmount); + proposal2.Result.ApproveCounts.ShouldBe(0); + } + + [Fact] + public async Task GetProposal_Success() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + + var result = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + result.ShouldNotBeNull(); + result.Id.ShouldBe(proposalId); + result.Title.ShouldBe("Test Proposal"); + result.Description.ShouldBe("This is a test proposal."); + result.StartTimestamp.ShouldBeLessThanOrEqualTo(BlockTimeProvider.GetBlockTime()); + result.EndTimestamp.ShouldBeGreaterThan(BlockTimeProvider.GetBlockTime()); + result.Proposer.ShouldBe(Accounts[0].Address); + result.Result.ApproveCounts.ShouldBe(0); + result.Result.RejectCounts.ShouldBe(0); + result.Result.AbstainCounts.ShouldBe(0); + } + + [Fact] + public async Task GetProposal_ProposalNotFound_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = "1"; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId })); + exception.Message.ShouldContain("Proposal not found."); + } + + [Fact] + public async Task GetProposal_MultipleProposals_Success() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId1 = await CreateTestProposalAsync(); + var proposalId2 = await CreateTestProposalAsync(); + + var result1 = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId1 }); + result1.ShouldNotBeNull(); + result1.Id.ShouldBe(proposalId1); + + var result2 = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId2 }); + result2.ShouldNotBeNull(); + result2.Id.ShouldBe(proposalId2); + } + + [Fact] + public async Task GetProposal_ExpiredProposal_ShouldSucceed() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + + // fast forward to proposal end time + BlockTimeProvider.SetBlockTime((DefaultProposalEndTimeOffset + 1) * 1000); + + var result = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + result.ShouldNotBeNull(); + result.EndTimestamp.ShouldBeLessThanOrEqualTo(BlockTimeProvider.GetBlockTime()); + } + + [Fact] + public async Task GetProposal_ProposalWithVotes_ShouldSucceed() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var voteInput = new VoteInput { - ProposalId = proposal.Id, - Vote = false, - Voter = Accounts[2].Address + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount }; - await SimpleDAOStub.VoteOnProposal.SendAsync(voteInput1); - await SimpleDAOStub.VoteOnProposal.SendAsync(voteInput2); - var proposalResult = SimpleDAOStub.GetProposal.CallAsync(new StringValue{Value = proposal.Id}).Result; - proposalResult.YesVotes.ShouldContain(Accounts[1].Address); - proposalResult.NoVotes.ShouldContain(Accounts[2].Address); + await SimpleDAOStub.Vote.SendAsync(voteInput); + + var result = await SimpleDAOStub.GetProposal.CallAsync(new StringValue { Value = proposalId }); + result.ShouldNotBeNull(); + result.Result.ApproveCounts.ShouldBe(BallotAmount); + result.Result.RejectCounts.ShouldBe(0); + result.Result.AbstainCounts.ShouldBe(0); } [Fact] - public async Task VoteOnProposalTest_NoPermission() + public async Task GetTokenSymbol_Success() { - await JoinDAOTest_Success(); - var proposal = await CreateMockProposal(Accounts[1].Address); - var voteInput1 = new VoteInput + await InitializeAndApproveSimpleDaoContract(); + + var result = await SimpleDAOStub.GetTokenSymbol.CallAsync(new Empty()); + result.ShouldNotBeNull(); + result.Value.ShouldBe("ELF"); + } + + [Fact] + public async Task HasVoted_Success_NotVoted() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var input = new HasVotedInput { - ProposalId = proposal.Id, - Vote = true, - Voter = Accounts[3].Address + ProposalId = proposalId, + Address = Accounts[0].Address }; - var executionResult = await SimpleDAOStub.VoteOnProposal.SendWithExceptionAsync(voteInput1); - executionResult.TransactionResult.Error.ShouldContain("Only DAO members can vote"); + + var result = await SimpleDAOStub.HasVoted.CallAsync(input); + result.ShouldNotBeNull(); + result.Value.ShouldBeFalse(); } [Fact] - public async Task VoteOnProposalTest_NoProposal() + public async Task HasVoted_Success_Voted() { - await JoinDAOTest_Success(); - var proposal = await CreateMockProposal(Accounts[1].Address); + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + var voteInput = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + await SimpleDAOStub.Vote.SendAsync(voteInput); + + var input = new HasVotedInput + { + ProposalId = proposalId, + Address = Accounts[0].Address + }; + + var result = await SimpleDAOStub.HasVoted.CallAsync(input); + result.ShouldNotBeNull(); + result.Value.ShouldBeTrue(); + } + + [Fact] + public async Task HasVoted_ProposalNotFound_ShouldThrow() + { + await InitializeAndApproveSimpleDaoContract(); + + var input = new HasVotedInput + { + ProposalId = "1", + Address = Accounts[0].Address + }; + + var exception = await Assert.ThrowsAsync(async () => + await SimpleDAOStub.HasVoted.CallAsync(input)); + exception.Message.ShouldContain("Proposal not found."); + } + + [Fact] + public async Task HasVoted_MultipleVoters_ShouldSucceed() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId = await CreateTestProposalAsync(); + + var user1Stub = GetTester(ContractAddress, Accounts[1].KeyPair); + var user1TokenStub = GetTester(TokenContractAddress, Accounts[1].KeyPair); + await TokenContractApprove(user1TokenStub); + await SendTokenTo(Accounts[1].Address); + var user2Stub = GetTester(ContractAddress, Accounts[2].KeyPair); + var user2TokenStub = GetTester(TokenContractAddress, Accounts[2].KeyPair); + await TokenContractApprove(user2TokenStub); + await SendTokenTo(Accounts[2].Address); + var voteInput1 = new VoteInput { - ProposalId = "123", - Vote = true, - Voter = Accounts[1].Address + ProposalId = proposalId, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + await user1Stub.Vote.SendAsync(voteInput1); + + var voteInput2 = new VoteInput + { + ProposalId = proposalId, + Vote = VoteOption.Rejected, + Amount = BallotAmount + }; + await user2Stub.Vote.SendAsync(voteInput2); + + var input1 = new HasVotedInput + { + ProposalId = proposalId, + Address = Accounts[1].Address + }; + var result1 = await SimpleDAOStub.HasVoted.CallAsync(input1); + result1.ShouldNotBeNull(); + result1.Value.ShouldBeTrue(); + + var input2 = new HasVotedInput + { + ProposalId = proposalId, + Address = Accounts[2].Address + }; + var result2 = await SimpleDAOStub.HasVoted.CallAsync(input2); + result2.ShouldNotBeNull(); + result2.Value.ShouldBeTrue(); + } + + [Fact] + public async Task HasVoted_DifferentProposals_ShouldSucceed() + { + await InitializeAndApproveSimpleDaoContract(); + + var proposalId1 = await CreateTestProposalAsync(); + var proposalId2 = await CreateTestProposalAsync(); + + var voteInput = new VoteInput + { + ProposalId = proposalId1, + Vote = VoteOption.Approved, + Amount = BallotAmount + }; + await SimpleDAOStub.Vote.SendAsync(voteInput); + + var input1 = new HasVotedInput + { + ProposalId = proposalId1, + Address = Accounts[0].Address + }; + var result1 = await SimpleDAOStub.HasVoted.CallAsync(input1); + result1.ShouldNotBeNull(); + result1.Value.ShouldBeTrue(); + + var input2 = new HasVotedInput + { + ProposalId = proposalId2, + Address = Accounts[0].Address + }; + var result2 = await SimpleDAOStub.HasVoted.CallAsync(input2); + result2.ShouldNotBeNull(); + result2.Value.ShouldBeFalse(); + } + + private async Task> InitializeSimpleDaoContract() + { + return await SimpleDAOStub.Initialize.SendAsync(new InitializeInput + { + TokenSymbol = TokenSymbol + }); + } + + private async Task CreateTestProposalAsync(Timestamp startTime = null, Timestamp expireTime = null) + { + var input = new CreateProposalInput + { + Title = "Test Proposal", + Description = "This is a test proposal.", + EndTimestamp = expireTime ?? BlockTimeProvider.GetBlockTime().AddSeconds(100), + StartTimestamp = startTime ?? BlockTimeProvider.GetBlockTime() }; - var executionResult = await SimpleDAOStub.VoteOnProposal.SendWithExceptionAsync(voteInput1); - executionResult.TransactionResult.Error.ShouldContain("Proposal not found"); + + var result = await SimpleDAOStub.CreateProposal.SendAsync(input); + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var proposals = await SimpleDAOStub.GetAllProposals.CallAsync(new Empty()); + + return proposals.Proposals.Count.ToString(); + } + + private async Task InitializeAndApproveSimpleDaoContract() + { + await InitializeSimpleDaoContract(); + await TokenContractApprove(TokenContractStub); + } + + private async Task TokenContractApprove(TokenContractContainer.TokenContractStub tokenContractStub) + { + await tokenContractStub.Approve.SendAsync(new ApproveInput + { + Spender = ContractAddress, + Symbol = TokenSymbol, + Amount = 10 + }); + } + + private async Task SendTokenTo(Address address) + { + await TokenContractStub.Transfer.SendAsync(new TransferInput + { + To = address, + Symbol = TokenSymbol, + Amount = 100 + }); } } diff --git a/templates/SimpleDAOContract/test/_Setup.cs b/templates/SimpleDAOContract/test/_Setup.cs index 440ebad..0220674 100644 --- a/templates/SimpleDAOContract/test/_Setup.cs +++ b/templates/SimpleDAOContract/test/_Setup.cs @@ -1,25 +1,37 @@ -using AElf.Cryptography.ECDSA; -using AElf.Testing.TestBase; +using AElf.Contracts.MultiToken; +using AElf.ContractTestBase.ContractTestKit; +using AElf.Cryptography.ECDSA; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; namespace AElf.Contracts.SimpleDAO { // The Module class load the context required for unit testing - public class Module : ContractTestModule + public class Module : Testing.TestBase.ContractTestModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + base.ConfigureServices(context); + context.Services.AddSingleton(); + } } // The TestBase class inherit ContractTestBase class, it defines Stub classes and gets instances required for unit testing - public class TestBase : ContractTestBase + public class TestBase : Testing.TestBase.ContractTestBase { + internal IBlockTimeProvider BlockTimeProvider; + // The Stub class for unit testing internal readonly SimpleDAOContainer.SimpleDAOStub SimpleDAOStub; + internal readonly TokenContractContainer.TokenContractStub TokenContractStub; // A key pair that can be used to interact with the contract instance private ECKeyPair DefaultKeyPair => Accounts[0].KeyPair; public TestBase() { SimpleDAOStub = GetSimpleDAOContractStub(DefaultKeyPair); + TokenContractStub = GetTester(TokenContractAddress, DefaultKeyPair); + BlockTimeProvider = Application.ServiceProvider.GetService(); } private SimpleDAOContainer.SimpleDAOStub GetSimpleDAOContractStub(ECKeyPair senderKeyPair)