Simple Contract Header
Coding the Simple Contract's header file
Having created the SimpleContract
files and registered them in CMake, let's implement the contract's header first, as most of the registration is done there.
Declaring the Contract Class
Open the header file (simplecontract.h
) and add the following lines:
This is a simple skeleton so we can start building the proper contract. From top to bottom:
We create include guards as a safety measure
We include the
DynamicContract
class and three SafeVariable classes for the contract's inner variables -SafeString
,SafeUint256_t
andSafeTuple
, which represent a Soliditystring
,uint256
andtuple/struct
types, respectively (check thesrc/contract/variables
subfolder for all available variable abstractions)For SafeUint (and SafeInt as well), we include
src/utils/utils.h
as all of the SafeUintX/SafeIntX aliases are declared there, if you want to useSafeUint_t<X>
/SafeInt_t<X>
then includesrc/contract/variables/safeuint.h
(orsafeint.h
) directly instead
We create our
SimpleContract
class, inherit it fromDynamicContract
, and leave some space for the private and public members that will be coded next
Now we can declare the members of our contract. We have to pay attention to some rules described earlier, which we'll go through slowly, one part at a time.
Declaring the Contract Variables
Variables MUST be private
and MUST inherit one of the SafeVariable classes. So our class declaration would start with something like this:
Our three variables name_
, number_
and tuple_
are respectively declared as a SafeString
, SafeUint256_t
and SafeTuple<std::string, uint256_t>
. Notice we can use primitive types inside the tuple just fine, as the contract's inner variables already ensure commit/revert safety due to their types being inherited from their SafeVariable counterparts.
Declaring the Contract Functions
Functions in general can return either void
or an ABI-supported C++ type (see the correlation on Solidity ABI). The main difference between both is that view functions MUST be const
(e.g. getName() const;
), while non-view functions MUST NOT be const
(e.g. setName(std::string name);
).
For the two registering functions, registerContract()
MUST be public static void
, and registerContractFunctions()
MUST be private void override
. For the dump()
function, it MUST be const override
and return a DBBatch
object.
Just like with the tuple variable, we can use primitive types and returns on functions just fine, for the same reasons stated above. This also extends to constructors, whch we'll see below.
Declaring the Contract Constructor
Like any C++ derived class, we must call its base class constructor and pass the proper arguments to it (besides the arguments for the derived class itself) so it can be constructed properly. Any contract derived from DynamicContract
MUST have two constructors - one for creating a new contract from scratch, and another for loading the contract from the database:
Aside from our name
, number
and tuple
variables, both constructors also take a few other arguments required by the base DynamicContract constructor:
address
, creator
, chainId
and db
are internal variables used by the base class, and should always be declared last. They are equivalent to:
DynamicContract constructor argument | Taken from |
---|---|
address | derived using this->deriveContractAddress() |
creator | this->getCaller() |
chainId | this->options->getChainID() |
DB | this->db |
Keep in mind that, when calling the base class constructor later on, the contractName
argument MUST be EXACTLY the same as your contract's class name. This is because contractName
is used to load the contract type from the database, so incorrectly naming it will result in a segfault at load time.
Declaring the Contract Events
Events MUST be public
, void
, non-const
, AND call an internal function named emitEvent()
, which is available to all Dynamic Contracts and does the proper emission of the event.
emitEvent()
requires at most three arguments:
The event name (you can just pass
__func__
which is the same asthis->emitEvent("nameChanged", ...)
)(Optional) A tuple of
EventParam
objects representing the event's arguments, where:The first element is the argument type (e.g.
name
is astd::string
,number
is auint256_t
,tuple
is astd::tuple<std::string, uint256_t>
)The second element is a bool that indicates whether the argument should be indexed or not.
If your event has no arguments at all you can omit it or pass an empty tuple instead (e.g.
this->emitEvent(__func__, std::make_tuple())
)
(Optional) A flag that indicates whether the event is anonymous or not. Events are non-anonymous by default, so if you wish you can omit this (which is the case for our example, it is equivalent to
this->emitEvent(__func__, std::make_tuple(name), false)
- if it was an anonymous event, the last flag would betrue
instead)
Registering the Contract Class
One last thing we have to do within our header is properly register the contract class. All Dynamic Contracts use templating to automate most of the hard work for both our contract and the BDK.
First, define a public
tuple called ConstructorArguments
with the contract's variable types from its first constructor (the one from scratch - in our example, name is const std::string&
, number is const uint256_t&
, tuple is const std::tuple<std::string, uint256_t>&
):
Then, implement the registerContract()
function declared previously by calling another function from ContractReflectionInterface
called registerContractMethods()
, passing a few arguments to it, like this:
Inside the chevrons (registerContractMethods<...>()
):
The first argument is the contract's class type (in this case,
SimpleContract
)The following arguments are all the types of arguments inside its first constructor (from scratch - the same ones that were put inside
ConstructorArguments
) - you can copy-paste the constructor's arguments as-is and take out the namesIt's important to remember that contract arguments should be declared before the internal arguments used by the base class constructor, as stated in the previous step
Inside the parentheses (registerContractMethods<>(...)
):
The first argument is a string vector that is a list of all the exact names of the arguments in the constructor, each one separated by a comma - in this case,
"name_"
,"number_"
and"tuple_"
Note this does not include arguments used by the base class' constructor (e.g.
interface
,address
,creator
,chainId
,db
), only the ones inherent to the contract itself
The following arguments are tuples, one for each function from the contract, that contain respectively:
The exact name of the function (
"getName"
)A reference to the function itself (
&SimpleContract::getName
) - if you happen to have one or more overloads of the same function, you may need to specify which function is which usingstatic_cast
(e.g.getNumber()
would be registered asstatic_cast<uint256_t(SimpleContract::*)() const>(&SimpleContract::getNumber)
, whilegetNumber(const uint256_t&)
would be registered asstatic_cast<uint256_t(SimpleContract::*)(const uint256_t&) const>(&SimpleContract::getNumber)
)The state mutability of said function, accessed by a
FunctionTypes
enum (available values are"View"
,"NonPayable"
and"Payable"
)A string vector that is the list of arguments that the function takes, if any (or a blank list if none)
Every contract MUST have both ConstructorArguments
and registerContract()
implemented in order to be registered, even if ConstructorArguments
is empty (has no arguments at all).
If your contract has events, you should also register them so you can generate their ABI later on. Events are registered the same way as the contract class and its functions, but using a separate function from ContractReflectionInterface
called registerContractEvents()
.
Inside the chevrons (registerContractEvents<...>()
), the only argument is the contract's class name (in this case, SimpleContract
).
Inside the parentheses (registerContractEvents<>(...)
):
The first argument is the event's name (
"nameChanged"
)The second argument is a flag indicating whether the event is anonymous or not
The third argument is a reference to the event itself (
&SimpleContract::nameChanged
)The last argument is a string vector that is the list of parameters that the event takes, if any (or a blank list if none)
Finally, we go to the src/contract/customcontracts.h
file, include our contract's header and add it to the ContractTypes
tuple.
Last updated