Developing Smart Contracts
This article will guide you on how to develop a smart contract, and it uses the GreeterContract as an example. With the concepts presented in this article, you will be able to create your own basic contract.
Steps for developing smart contracts
The following content will walk you through the basics of writing a smart contract; this process contains essentially five steps:
Install template: Install the aelf smart contract templates locally using the dotnet command.
Initialize project: Build the project structure and generate the base contract code from the proto definition using the dotnet command.
Define the contract: The methods and types required in your contract should be defined in a protobuf file following the typical protobuf syntax.
Implement contract code: Implement the logic for the contract methods.
Testing smart contracts: Unit tests for contracts.
The Greeter
contract is a very simple contract that exposes an
AddGreeters
method to add a new greeter to GreeterList
, and a
GetGreeters
method to get all of greeters.
Install template
Installing a template means downloading templates from the NuGet repository to your local environment and installing them locally. Run the following command to install it.
dotnet new install AElf.ContractTemplates
After installation, you can use dotnet new uninstall
to verify the presence of this template locally.
Currently installed items:
AElf.ContractTemplates
Version: 1.0.0-alpha
Details:
Author: AElf
NuGetSource: https://api.nuget.org/v3/index.json
Templates:
AElf Contract (aelf) C#
Uninstall Command:
dotnet new uninstall AElf.ContractTemplates
If you can see this result, it indicates that the template installation was successful. This information shows the template name, version, and other details.
Initialize project
After installing, we need to initialize the project. Initializing the project is like
creating a specific contract project based on the template. This process is similar to
using the new()
method in OOP to create an instance of a class.
Using dotnet new
command to create a specific contract project. We can create custom contract
projects based on the template using -n
and -N
options. And -n
stands for contract name, -N
stands for namespace.
Run the following command, you can create a contract project that named GreeterContract
. In this case,
the contract name will be GreeterContract
. And the namespace of the project will be AElf.Contracts.Greeter
.
dotnet new aelf -n GreeterContract -N AElf.Contracts.Greeter
After running dotnet new command, we can get a new project generated base on template. The project structure is as follows.
.
├── src
│ ├── GreeterContract.cs
│ ├── GreeterContract.csproj
│ ├── GreeterContractState.cs
│ └── Protobuf
│ ├── contract
│ │ └── hello_world_contract.proto
│ └── message
│ └── authority_info.proto
└── test
├── GreeterContract.Tests.csproj
├── GreeterContractTests.cs
├── Protobuf
│ ├── message
│ │ └── authority_info.proto
│ └── stub
│ └── hello_world_contract.proto
└── _Setup.cs
The src folder
The src folder contains several protobuf files used to describe smart contract methods and data structures. It also includes specific implementations of smart contract methods and definition files for managing contract state in communication with the blockchain. For example, GreeterContractState.cs is one such file.
src
├── GreeterContract.cs
├── GreeterContract.csproj
├── GreeterContractState.cs
└── Protobuf
The test folder
Similarly, the test folder contains a proto subfolder, along with a setup file used to establish the unit testing environment for blockchain smart contracts. It defines test module classes and a base test class, facilitating context loading, stub class retrieval, and stub acquisition methods. These classes and methods are employed in unit tests to conduct various tests on the smart contract.
test
├── _Setup.cs
├── GreeterContract.Tests.csproj
├── GreeterContractTests.cs
└── Protobuf
Defining the contract
AElf defines smart contracts as services that are implemented using gRPC and Protobuf. These definitions are placed in the proto files and do not contain logic. The proto files are used to generate C# classes that will be used to implement the logic and state of the contract.
In the Protobuf folder, different subfolders are used to store various definition proto files. If a corresponding folder does not exist, you can create one yourself. In this context, only the contract and message directories are used. Here’s a breakdown of the Protobuf content under the src folder:
contract: The contract folder is used to store definition proto file of contract.
message: The proto files under the message folder are used to define common properties for import and use by other proto files.
reference: The reference folder is used to store the proto files of the referenced contract.
base: The base folder is used to store the basic proto files, such as ACS (aelf standard contract) proto files.
src
└── Protobuf
├── contract
│ └── hello_world_contract.proto
└── message
└── authority_info.proto
The hello_world_contract.proto file is used as a template for the HelloWorld contract. First, we need to delete this proto file. Next, we will create a new greeter_contract.proto file, which will be used for the GreeterContract contract. Let’s explore how to write definitions in the proto file.
syntax = "proto3";
import "aelf/options.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
// The namespace of this class
option csharp_namespace = "AElf.Contracts.Greeter";
service GreeterContract {
// The name of the state class the smart contract is going to use to access blockchain state
option (aelf.csharp_state) = "AElf.Contracts.Greeter.GreeterContractState";
// Actions (methods that modify contract state)
// Stores the value in contract state
rpc AddGreeters (google.protobuf.StringValue) returns (google.protobuf.Empty) {
}
// Views (methods that don't modify contract state)
// Get the value stored from contract state
rpc GetGreeters (google.protobuf.Empty) returns (GreeterList) {
option (aelf.is_view) = true;
}
}
message GreeterList {
repeated string greeter = 1;
}
The complete contract definition consists of three main parts:
Imports: These are the dependencies of your contract.
Service definitions: These define the methods of your contract.
Types: These are custom-defined types used by the contract.
Now, let’s take a closer look at these three different parts.
Syntax, imports and namespace
syntax = "proto3";
import "aelf/options.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
// The namespace of this class
option csharp_namespace = "AElf.Contracts.Greeter";
The first line specifies the syntax used in this protobuf file. We recommend using proto3 for your contracts. Next, you will notice that this contract specifies some imports. Let’s briefly describe them:
aelf/options.proto: Contracts can use aelf specific options. This file contains the definitions, including options like is_view that we will use later.
empty.proto, wrappers.proto: These are proto files imported directly from the protobuf library. They are useful for defining things like an empty return value and wrappers around common types, such as strings.
The last line specifies an option that determines the target namespace of the generated code. In this case, the generated code will be placed in the AElf.Contracts.Greeter namespace.
Service definitions
service GreeterContract {
// The name of the state class the smart contract is going to use to access blockchain state
option (aelf.csharp_state) = "AElf.Contracts.Greeter.GreeterContractState";
// Actions (methods that modify contract state)
// Stores the value in contract state
rpc AddGreeters (google.protobuf.StringValue) returns (google.protobuf.Empty) {
}
// Views (methods that don't modify contract state)
// Get the value stored from contract state
rpc GetGreeters (google.protobuf.Empty) returns (GreeterList) {
option (aelf.is_view) = true;
}
}
In the first line, we use the aelf.csharp_state
option to specify the full name of the state class.
This indicates that the state of the contract should be defined in the GreeterContractState
class under the AElf.Contracts.Greeter
namespace.
Next, an action method is defined: AddGreeters
. A contract method is composed of three parts: the method name,
the input argument type(s), and the output type. For instance, AddGreeters
specifies that it requires a google.protobuf.StringValue
input type, indicating that this method takes an argument, and the output type will be google.protobuf.Empty
.
The service also defines a view method: GetGreeters
. This method is exclusively used to query the contract state
and has no side effects on the state. The definition of GetGreeters
uses the aelf.is_view
option to designate it as a view method.
To summarize:
Use google.protobuf.Empty to specify that a method takes no arguments (import google/protobuf/empty.proto).
Use google.protobuf.StringValue to handle strings (import google/protobuf/wrappers.proto).
Use the aelf.is_view option to create a view method (import aelf/options.proto).
Use the aelf.csharp_state option to specify the namespace of your contract’s state (import aelf/options.proto).”
Custom types
message GreeterList {
repeated string greeter = 1;
}
A brief summary follows:
Use repeated to denote a collection of items of the same type.
Implement contract code
After defining the contract’s structure and methods, you need to execute the dotnet build command within the src folder. This will recompile the proto files and generate updated C# code. You should repeat this command every time you make changes to the contract’s structure to ensure the code is up to date.
Currently, you can extend the generated code to implement the contract’s logic. There are two key files involved:
GreeterContract: This file contains the actual implementation logic. It inherits from the contract base generated by the proto files.
GreeterContractState: This is the state class that holds properties for reading and writing the contract’s state. It inherits the ContractState class from the C# SDK.
using AElf.Sdk.CSharp;
using Google.Protobuf.WellKnownTypes;
namespace AElf.Contracts.Greeter
{
// Contract class must inherit the base class generated from the proto file
public class GreeterContract : GreeterContractContainer.GreeterContractBase
{
// A method that modifies the contract state
public override Empty AddGreeters(StringValue input)
{
// Should not greet to empty string or white space.
Assert(!string.IsNullOrWhiteSpace(input.Value), "Invalid name.");
// State.GreetedList.Value is null if not initialized.
var greeterList = State.GreeterList.Value ?? new GreeterList();
// Add input.Value to State.GreetedList.Value if it's new to this list.
if (!greeterList.Greeter.Contains(input.Value))
{
greeterList.Greeter.Add(input.Value);
}
// Update State.GreetedList.Value by setting it's value directly.
State.GreeterList.Value = greeterList;
return new Empty();
}
// A method that read the contract state
public override GreeterList GetGreeters(Empty input)
{
return State.GreeterList.Value ?? new GreeterList();
}
}
}
using AElf.Sdk.CSharp.State;
namespace AElf.Contracts.Greeter
{
public class GreeterContractState : ContractState
{
public SingletonState<GreeterList> GreeterList { get; set; }
}
}
Asserting
Assert(!string.IsNullOrWhiteSpace(input.Value), "Invalid name.");
When writing a smart contract, it is often useful and recommended to validate the input. AElf smart contracts can utilize
the Assert
method defined in the base smart contract class to implement this pattern. For example, in the above method,
validation checks if the input string is null or consists only of white spaces. If this condition evaluates to false,
the transaction execution will be terminated.
Saving and reading state
State.GreeterList.Value = greeterList;
...
var greeterList = State.GreeterList.Value;
From within the contract methods, you can easily save and read the contract’s state using the State property of the contract. In this context, the State property refers to the GreeterContractState class. The first line is used to save the input value to the state, while the second line is used to retrieve the value from the state.
Contract state
As a reminder, here is the state definition in the contract where we specify the name of the class and its type,
along with the custom type GreeterList
:
public class GreeterContractState : ContractState
{
public SingletonState<GreeterList> GreeterList { get; set; }
}
The aelf.csharp_state option allows the contract author to specify the namespace and class name for the state. To implement a state class, you need to inherit from the ContractState class provided by the C# SDK. When defining properties under the state, we follow a generic approach:
To save and read a single object: use
SingletonState<ClassType>
.To save and read a key-value pair: use
MappedState<KeyClassType, ValueClassType>
.
After becoming familiar with all state usages, you can also use StringState
as an alternative to SingletonState<ClassType>
.
Testing smart contracts
This tutorial will demonstrate how to test the GreeterContract for reference.
AElf.ContractTestKit
is a testing framework designed specifically for testing aelf smart contracts. With this framework,
you can simulate the execution of a transaction by constructing a stub of a smart contract and utilize the methods provided
by the Stub instance (corresponding to the contract’s Action methods) for executing transactions and
queries (corresponding to the Views methods of the contract) to obtain transaction execution results in the test case.
As you can observe, the test code is located within the test folder. Typically, this test folder contains a project file (.csproj) and at least two .cs files. The project file serves as a standard C# xUnit test project file, with additional references included as needed.
test
├── GreeterContract.Tests.csproj
├── GreeterContractTests.cs
├── Protobuf
│ ├── message
│ │ └── authority_info.proto
│ └── stub
│ └── hello_world_contract.proto
└── _Setup.cs
Steps of testing smart contracts The testing process closely mirrors the development process and generally consists of the following steps:
Defining the contract: All the required methods and types for your contract should be defined in a protobuf file. These definitions are identical to those in the src folder, and you can simply copy them to the test folder.
Setting up the testing context: To conduct local contract testing, it’s essential to simulate the execution of a transaction by creating a stub. In this step, you will configure the necessary context and stub components needed for testing.
Implementing contract unit test code: Create the logic for unit test methods, which will test the contract’s functionality and ensure it works as expected.
Defining the contract
The Protobuf folder within the test directory serves a similar purpose to the src directory but with slightly different folder names. For the Protobuf section within the test folder, the following applies:
message: The proto files contained in the message folder are used to define common properties that can be imported and utilized by other proto files.
stub: The stub folder houses contract proto files dedicated to unit testing. Additionally, it may contain other proto files that this test proto file depends on and imports.
test
└── Protobuf
├── message
│ └── authority_info.proto
└── stub
└── hello_world_contract.proto
You can copy the necessary proto files from the src folder and paste them into the stub folder. It’s important to ensure that contract proto files from the src folder and any dependent proto files are correctly placed in the stub directory.
Setting up testing context
To locally test contract methods, you need to establish the context required for testing. This process primarily involves obtaining the stub for the contract. Below is the content of the _Setup.cs file:
using AElf.Cryptography.ECDSA;
using AElf.Testing.TestBase;
namespace AElf.Contracts.Greeter
{
// The Module class load the context required for unit testing
public class Module : ContractTestModule<GreeterContract>
{
}
// The TestBase class inherit ContractTestBase class, it defines Stub classes and gets instances required for unit testing
public class TestBase : ContractTestBase<Module>
{
// The Stub class for unit testing
internal readonly GreeterContractContainer.GreeterContractStub GreeterContractStub;
// A key pair that can be used to interact with the contract instance
private ECKeyPair DefaultKeyPair => Accounts[0].KeyPair;
public TestBase()
{
GreeterContractStub = GetGreeterContractContractStub(DefaultKeyPair);
}
private GreeterContractContainer.GreeterContractStub GetGreeterContractContractStub(ECKeyPair senderKeyPair)
{
return GetTester<GreeterContractContainer.GreeterContractStub>(ContractAddress, senderKeyPair);
}
}
}
In this code, TestBase inherits ContractTestBase<Module> and defines a contract stub within the class.
It also obtains a key pair from the AElf.ContractTestKit
framework. In the constructor, the address and
key pair parameters are provided, and the GetTester
method is used to retrieve the contract stub.
Implement contract unit test code
Now comes the easy part: the test class only needs to inherit from TestBase. Once you’ve done that, you can proceed to write the unit test implementations you require.
In this section, you can use the AddGreetersTest
method to save a message to the state. Following that,
you can call the GetGreeters
method to retrieve the message from the state. Finally, you can compare the retrieved message
with the originally input message to verify whether the values match.
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using Shouldly;
using Xunit;
namespace AElf.Contracts.Greeter
{
// This class is unit test class, and it inherit TestBase. Write your unit test code inside it
public class GreeterContractTests : TestBase
{
[Fact]
public async Task AddGreetersTest()
{
// Arrange
var user1 = new StringValue { Value = "Tom" };
var user2 = new StringValue { Value = "Jerry" };
var expectList = new GreeterList();
expectList.Greeter.Add(user1.Value);
expectList.Greeter.Add(user2.Value);
// Act
await GreeterContractStub.AddGreeters.SendAsync(user1);
await GreeterContractStub.AddGreeters.SendAsync(user2);
// Assert
var greeterList = await GreeterContractStub.GetGreeters.CallAsync(new Empty());
greeterList.ShouldBe(expectList);
}
}
}