In this tutorial we will implement a simple contract, write unit tests for it, and run through a debugging process to find errors.
All you need to run this example is Java and an IDE. Simply create a Java project that depends on the AVM’s latest tooling jars as a library.
1. Writing the Smart Contract
Let’s start by creating the smart contract itself. Below is an example of a smart contract that can be used for a simple voting DApp.
Contract functionality:
- Only the owner of the contract (account deploying the contract) may add or remove members,
- Only members can introduce new proposals,
- Only members can vote on proposals,
- If more than 50% of members are in favor of a proposal and vote for it, the proposal will pass.
The static
block in the contract is only executed once at deployment. We set the initial members, minimumQuorum
, and owner
in this block. Although we initiated our contract with a set of members, the owner can also add and remove members afterwards.
We keep track of the members and their proposals using an AionSet
andAionMap
. Each proposal can be accessed from the map using its unique identifier.
The main functions in the contract are:
addProposal
which allows a member to add a proposal description for a vote.vote
which allows a member to vote on an available proposal by passing its Id. A proposal that has gotten majority of members’ votes will pass. Notice that aProposalPassed
event is generated to log the Id of the passed proposal.
2. Writing Unit Tests
We will use the AvmRule
to write our test. AvmRule
is a Junit Rule designed for testing contracts on an embedded AVM. It creates an in-memory representation of the Aion kernel and AVM. Every time we run a test, the built kernel and AVM instances are refreshed.
Before testing our contract, we need to deploy it to an in-memory Aion blockchain and we’ll use the AvmRule
to accomplish this.
A. Instantiate the AvmRule
You can see that the rule takes in a boolean argument which enables/disables the debug mode. It’s good practice to write your tests with the debugger enabled. You can see how to debug your contract in the next section.
@Rulepublic AvmRule avmRule = new AvmRule(true);
Note: This line will instantiate an embedded AVM for each test method. If the rule is defined as a @classRule
, only one instance of the AVM and kernel will be instantiated for the test class.
B. Getting Contract Bytes
Now we have to get the bytes that correspond to the in-memory representation of the contract jar. In order to get the bytes, we will use the getDappBytes
method from the AvmRule
.
getDappBytes
takes the following parameters:
- Main class of the contract,
- Contract constructor arguments, which can be accessed and decoded in the
static
block, - Other classes that need to be included in the DApp jar.
public byte[] getDappBytes(Class<?> mainClass, byte[] arguments, Class<?>... otherClasses)
C. Deploying your contract
Deploying the contract can easily be done using the deploy
function.
public ResultWrapper deploy(Address from, BigInteger value, byte[] dappBytes)
AvmRule
also gives the ability to create accounts with initial balances in the Aion kernel.
Here’s how to deploy our Voting contract with a set of 3 members.
D. Calling Methods
We can call methods in the contract by:
- Encoding the method name and its arguments,
- Passing the encoded bytes to the
AvmRule
’scall
method.
public ResultWrapper call(Address from, Address dappAddress, BigInteger value, byte[] transactionData)
As an example, let’s create a new proposal. We will validate the proposal by checking that the NewProposalAdded
event is generated and the event topics and data are correct.
Now we’ll submit a proposal along with two votes for it. Since two distinct members voted for proposal with Id 0, the proposal should pass. Thus, we expect two different events to be generated for the last transaction- Voted
and ProposalPassed
.
You can also query a proposal’s status by its Id. You’ll see that the decoded data that is returned is true
, meaning that the proposal has passed.
3. Debugging the Contract
It’s super easy to debug our contract — simply set the breaking points in the source code! Since we created our AvmRule
with debugging enabled, the execution will be halted when the breaking point is reached. Let’s run through an example.
This is the state of the smart contract after deployment.
You can see that the contract has:
- 3 members,
- 0 proposals,
minimumQuorum = 2
.
You can inspect the contents of each collection as well. For instance — by calling the addProposal
, you will be able to see the updated AionMap
.
Let’s actually put the debugger to the test. We’ll be purposely creating a simple mistake in the way the proposal is evaluated as passed. I’ll modify the proposal passing criteria as shown below. Notice that the equals condition has been changed to less than or equal.
if (!votedProposal.isPassed && votedProposal.voters.size() <= minimumQuorum)
Now when the first owner submits their vote, the proposal will pass.
Let’s debug the method call and run through the function step by step.
You can see that, although minimumQuorum
is equal to 2, the voter count for the proposal is only 1. Our modified if statement (from above) has been satisfied, and on line 51
the isPassed
flag is set to true.
From there, you can easily identify where the bug was in your code.
Conclusion
If you’ve ever developed smart contracts for Ethereum, you’d know the pain of writing the contract in an unfamiliar, purpose-built language and debugging it.
Anybody familiar with Java, would feel right at home writing smart contracts using the AVM. Plus, all the debugging features in any IDE on the market can be used to test and debug a Java smart contract. Go ahead and give it a try yourself!