Creating a Dynamic Contract (Simple)
Let's create a simple Solidity contract that allows two private variables to be changed by the owner of the contract. We will call this contract
SimpleContract
.We'll be using the following Solidity code as a reference:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
â
contract SimpleContract {
address owner;
string name;
uint256 value;
â
constructor(string memory argName, uint256 argValue) {
owner = msg.sender;
name = argName;
value = argValue;
}
â
function getName() public view returns(string memory) {
return name;
}
â
function getValue() public view returns(uint256) {
return value;
}
â
function setName(string memory argName) public {
require(msg.sender == owner, "Not owner");
name = argName;
}
â
function setValue(uint256 argValue) public {
require(msg.sender == owner, "Not owner");
value = argValue;
}
}
The contract has a pretty simple structure - a constructor that sets its owner, and two functions that allow the owner to change the
name
and value
variables.To recreate this contract within OrbiterSDK, first we need to create its header (
.h
) and source (.cpp
) files, as is customary in C++ development - the header will include the definition of our contract class, and the source will include its implementation.Create two new files -
src/contract/simplecontract.h
and src/contract/simplecontract.cpp
- and add them to src/contract/CMakeLists.txt
so they can be compiled with the project, like this:set(CONTRACT_HEADERS
...
${CMAKE_SOURCE_DIR}/src/contract/simplecontract.h
...
)
set(CONTRACT_SOURCES
...
${CMAKE_SOURCE_DIR}/src/contract/simplecontract.cpp
...
)
Then, open the file
src/contract/simplecontract.h
and add the following lines:#ifndef SIMPLECONTRACT_H
#define SIMPLECONTRACT_H
â
#include "dynamiccontract.h"
#include "variables/safestring.h"
#include "variables/safeuint256_t.h"
â
class SimpleContract : public DynamicContract {
private:
...
public:
...
};
â
#endif // SIMPLECONTRACT_H
This is a simple skeleton so we can start building the proper contract. Notice we use include guards as a safety measure, we include and inherit the
DynamicContract
class, and two Safe Variable classes for the contract's inner variables - SafeString
and SafeUint256_t
.Now, we can declare the variables of the contract and their respective functions. We have to pay attention to some rules described earlier in 3-2:
- Use private Safe Variables for the contract variables
- View functions MUST be
const
and return astd::string
with ABI encoded data - Non-view functions MUST be non-
const
and returnvoid
registerContractFunctions()
must be declared and override the base class function
So our class declaration would turn into something like this:
class SimpleContract : public DynamicContract {
private:
SafeString name; // string name
SafeUint256_t value; // uint256 value
void registerContractFunctions() override;
â
public:
std::string getName() const; // function getName() public view returns(string memory)
std::string getValue() const; // function getValue() public view returns(uint256)
void setName(const std::string& argName); // function setName(string memory argName) public
void setValue(uint256_t argValue); // function setValue(uint256 argValue) public
};
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
(as is the case here) must have two constructors - one for loading the contract from the database, and another for creating a new contract from scratch.Aside from that, the contract's name (passed to the base class constructor as
contractName
) MUST be EXACTLY the same as the name of your contract class. This is because contractName
is used to load the contract type from the database, thus incorrectly naming it will result in a segfault at load time.Both constructors from the base DynamicContract class look like this:
// Constructor that loads the contract from the database
DynamicContract(
ContractManager::ContractManagerInterface &interface,
const Address& address,
const std::unique_ptr<DB> &db
);
â
// Constructor that creates the contract from scratch
DynamicContract(
ContractManager::ContractManagerInterface &interface,
const std::string& contractName,
const Address& address,
const Address& creator,
const uint64_t& chainId,
const std::unique_ptr<DB> &db
);
We must also manually declare the destructor of our own class and mark it as
override
, that way the compiler knows we are overriding the base class destructor and can properly call it.That all said, our full example contract header will look something like this:
#ifndef SIMPLECONTRACT_H
#define SIMPLECONTRACT_H
â
#include "dynamiccontract.h"
#include "variables/safestring.h"
#include "variables/safeuint256_t.h"
â
class SimpleContract : public DynamicContract {
private:
SafeString name; // string name
SafeUint256_t value; // uint256 value
void registerContractFunctions() override;
â
public:
// Constructor from scratch. Create new contract with given name and value.
SimpleContract(
const std::string& name,
uint256_t value,
ContractManager::ContractManagerInterface &interface,
const Address& address,
const Address& creator,
const uint64_t& chainId,
const std::unique_ptr<DB> &db
);
â
// Constructor from load. Load contract from database.
SimpleContract(
ContractManager::ContractManagerInterface &interface,
const Address& address,
const std::unique_ptr<DB> &db
);
â
// Destructor.
~SimpleContract() override;
â
std::string getName() const; // function getName() public view returns(string memory)
std::string getValue() const; // function getValue() public view returns(uint256)
void setName(const std::string& argName); // function setName(string memory argName) public
void setValue(uint256_t argValue); // function setValue(uint256 argValue) public
};
â
#endif // SIMPLECONTRACT_H
Now we can proceed to the definitions in the source file. Open
src/contract/simplecontract.cpp
and #include simplecontract.h
right at the beginning.First, we need to implement the constructors and destructor of our contract class, while having some things in mind:
- The base
DynamicContract
constructor must be called with the arguments of the base class constructor - Private contract variables must be accessed with
this
(e.g.this->name
) - The
registerContractFunctions()
andupdateState(true)
functions must be called at the end
Our source file will look something like this:
#include "simplecontract.h"
â
SimpleContract::SimpleContract(
const std::string& name,
uint256_t value,
ContractManager::ContractManagerInterface &interface,
const Address& address,
const Address& creator,
const uint64_t& chainId,
const std::unique_ptr<DB> &db
) : DynamicContract(interface, "SimpleContract", address, creator, chainId, db), name(this), value(this) {
this->name = name;
this->value = value;
registerContractFunctions();
this->updateState(true);
}
â
SimpleContract::SimpleContract(
ContractManager::ContractManagerInterface &interface,
const Address& address,
const std::unique_ptr<DB> &db
) : DynamicContract(interface, address, db), name(this), value(this) {
this->name = db->get("name", DBPrefix::contracts + this->getContractAddress().get());
this->value = Utils::bytesToUint256(db->get("value", DBPrefix::contracts + this->getContractAddress().get()));
registerContractFunctions();
this->updateState(true);
}
â
SimpleContract::~SimpleContract() {
this->db->put("name", this->name.get(), DBPrefix::contracts + this->getContractAddress().get());
this->db->put("value", Utils::uint256ToBytes(this->value.get()), DBPrefix::contracts + this->getContractAddress().get());
return;
}
Notice that, in the first constructor, we use "SimpleContract" as the
contractName
argument in the base DynamicContract
constructor. As stated in Step 2.2, this match is a requirement, otherwise it will result in a segfault.The destructor is responsible for saving the variables of the contract to the database, so that they can be loaded later by the second constructor, when
ContractManager
is being constructed.Now we can implement the proper functions of our contract. Let's start with the view functions (that only read and never change the contract's variables when called).
Remember that view functions MUST be
const
and return a std::string
with ABI encoded data. In this case, the return values are the variables of the contract, so we can use the ABI::Encoder
class to encode the variables and return the raw data. See 3-7 for more details on ABI.The implementation would look something like this:
std::string SimpleContract::getName() const {
return ABI::Encoder({this->name.get()}).getRaw();
}
â
std::string SimpleContract::getValue() const {
return ABI::Encoder({this->value.get()}).getRaw();
}
After that, we'll implement the non-view functions (that do change the contract's variables when called).
Remember that non-view functions MUST be non-
const
and return void
. In this case, we must also check that whoever is calling those functions is the actual creator of the contract.As said in 3-2, any contract within OrbiterSDK has access to the global variables
caller
and contractCreator
(via getters), which are the address of the caller of the function, and the address of the creator of the contract, respectively. So we can use those to prevent calls from unwanted addresses.The implementation would look something like this:
void SimpleContract::setName(const std::string& argName) {
if (this->getCaller() != this->getContractCreator()) {
throw std::runtime_error("Only contract creator can call this function.");
}
this->name = argName;
}
â
void SimpleContract::setValue(uint256_t argValue) {
if (this->getCaller() != this->getContractCreator()) {
throw std::runtime_error("Only contract creator can call this function.");
}
this->value = argValue;
}
After that, we must implement the
registerContractFunctions()
function, which is responsible for registering the contract's functions so they can be called later by a transaction or a RPC eth_call
.Functions should be registered using the same Solidity function signature, which means:
- The name of the function and types of the arguments...
- ...separated by commas and enclosed in parentheses (e.g. "func(string,uint256)")...
- ...hashed with keccak256 (in our case,
Utils::sha3()
) and then converted to a string... - ...taking only the first 4 (8 hex characters) bytes of the hash (also called the "functor").
This is all done by calling one of the following functions on the derived contract:
void registerFunction(const std::string& functor, std::function<void(const ethCallInfo& tx)> f);
void registerPayableFunction(const std::string& functor, std::function<void(const ethCallInfo& tx)> f);
void registerViewFunction(const std::string& functor, std::function<std::string(const ethCallInfo& str)> f);
Each function should be used for their effective purpose:
registerFunction()
is used to register a callable function (a function that is called by a transaction)registerPayableFunction()
is used to register a callable AND payable functionregisterViewFunction()
is used to register a view/const function
The
functor
argument should be the function signature by Solidity standards, as in:getContractBalance(address token)
->keccak256("getContractBalance(address)").substr(0,4)
->0x43ab265f
getUserBalance(address token, address user)
->keccak256("getUserBalance(address,address)").substr(0,4)
->0x6805d6ad
- And so on and so forth...
The
function
argument should be a lambda function declared on the spot, responsible for parsing the ethCallInfo
argument and calling the proper function.ethCallInfo
is a std::tuple
with the following information:Index | Description | Type |
---|---|---|
0 | From (where the call is coming from) | Address |
1 | To (where the call is going to) | Address |
2 | GasLimit | uint256_t |
3 | GasPrice | uint256_t |
4 | Value | uint256_t |
5 | Data | std::string |
You can access each information by using
std::get<index>(ethCallInfo)
, e.g. std::get<5>(ethCallInfo)
will get the data itself. Then, data.substr(0,4)
will be the function signature and the remaining data will be the ABI encoded parameters.We also provide the ABI namespace, which contains an encoder and decoder which you can use to encode and/or decode Solidity's ABI strings in order to call a function. See the following example:
void SimpleContract::registerContractFunctions() {
this->registerViewFunction(Utils::sha3("getName()").get().substr(0,4), [this](const ethCallInfo &callInfo) {
return this->getName();
});
this->registerViewFunction(Utils::sha3("getValue()").get().substr(0,4), [this](const ethCallInfo &callInfo) {
return this->getValue();
});
this->registerFunction(Utils::sha3("setName(string)").get().substr(0,4), [this](const ethCallInfo &callInfo) {
std::vector<ABI::Types> types = { ABI::Types::string };
ABI::Decoder decoder(types, std::get<5>(callInfo).substr(4));
return this->setName(decoder.getData<std::string>(0));
});
this->registerFunction(Utils::sha3("setValue(uint256)").get().substr(0,4), [this](const ethCallInfo &callInfo) {
std::vector<ABI::Types> types = { ABI::Types::uint256 };
ABI::Decoder decoder(types, std::get<5>(callInfo).substr(4));
return this->setValue(decoder.getData<uint256_t>(0));
});
}
That all said, our full example contract header will look something like this:
#include "simplecontract.h"
â
SimpleContract::SimpleContract(
const std::string& name,
uint256_t value,
ContractManager::ContractManagerInterface &interface,
const Address& address,
const Address& creator,
const uint64_t& chainId,
const std::unique_ptr<DB> &db
) : DynamicContract(interface, "SimpleContract", address, creator, chainId, db), name(this), value(this) {
this->name = name;
this->value = value;
registerContractFunctions();
this->updateState(true);
}
â
SimpleContract::SimpleContract(
ContractManager::ContractManagerInterface &interface,
const Address& address,
const std::unique_ptr<DB> &db
) : DynamicContract(interface, address, db), name(this), value(this) {
this->name = db->get("name", DBPrefix::contracts + this->getContractAddress().get());
this->value = Utils::bytesToUint256(db->get("value", DBPrefix::contracts + this->getContractAddress().get()));
registerContractFunctions();
this->updateState(true);
}
â
SimpleContract::~SimpleContract() {
this->db->put("name", this->name.get(), DBPrefix::contracts + this->getContractAddress().get());
this->db->put("value", Utils::uint256ToBytes(this->value.get()), DBPrefix::contracts + this->getContractAddress().get());
return;
}
â
std::string SimpleContract::getName() const { return ABI::Encoder({this->name.get()}).getRaw(); }
â
std::string SimpleContract::getValue() const { return ABI::Encoder({this->value.get()}).getRaw(); }
â
void SimpleContract::setName(const std::string& argName) {
if (this->getCaller() != this->getContractCreator()) {
throw std::runtime_error("Only contract creator can call this function.");
}
this->name = argName;
}
â
void SimpleContract::setValue(uint256_t argValue) {
if (this->getCaller() != this->getContractCreator()) {
throw std::runtime_error("Only contract creator can call this function.");
}
this->value = argValue;
}
â
void SimpleContract::registerContractFunctions() {
this->registerViewFunction(Utils::sha3("getName()").get().substr(0,4), [this](const ethCallInfo &callInfo) {
return this->getName();
});
this->registerViewFunction(Utils::sha3("getValue()").get().substr(0,4), [this](const ethCallInfo &callInfo) {
return this->getValue();
});
this->registerFunction(Utils::sha3("setName(string)").get().substr(0,4), [this](const ethCallInfo &callInfo) {
std::vector<ABI::Types> types = { ABI::Types::string };
ABI::Decoder decoder(types, std::get<5>(callInfo).substr(4));
return this->setName(decoder.getData<std::string>(0));
});
this->registerFunction(Utils::sha3("setValue(uint256)").get().substr(0,4), [this](const ethCallInfo &callInfo) {
std::vector<ABI::Types> types = { ABI::Types::uint256 };
ABI::Decoder decoder(types, std::get<5>(callInfo).substr(4));
return this->setValue(decoder.getData<uint256_t>(0));
});
}
We have successfully implemented our contract, but in order to be both deployable and callable, we must integrate it within the
ContractManager
class. To do that, we must:- Implement new functions to create and validate a transaction call to create a new contract instance
- Modify the ContractManager constructor to load the contract from database
- Modify the ContractManager
ethCall()
function to call the create/validate functions
In
src/contract/contractmanager.h
, we must add two private functions within the ContractManager class that take ethCallInfo
as argument:class ContractManager : BaseContract {
private:
...
// Create a new SimpleContract Contract
// function createNewSimpleContractContract(string memory name, uint256 value) public {}
void createNewSimpleContractContract(const ethCallInfo& callInfo);
// Check if transaction can actually create a new SimpleContract contract.
void validateCreateNewSimpleContractContract(const ethCallInfo& callInfo) const;
...
};
The
createNew...Function()
function is responsible for creating the contract itself, and the validateCreateNew...Function()
is responsible for checking a few details to ensure a transaction that was made can actually create it.The function signature for your
createNew...Contract
function should be the same as the one in Solidity. You can generate the ABI for your ContractManager using any Web3 development tool.In
src/contract/contractmanager.cpp
, we must #include "contractHeaderFile.h"
(in our case, simplecontract.h
) and the previous declared functions.#include "simplecontract.h"
â
...some source code...
â
void ContractManager::createNewSimpleContractContract(const ethCallInfo& callInfo) {
// Check if caller is creator
if (this->caller != this->getContractCreator()) {
throw std::runtime_error("Only contract creator can create new contracts");
}
â
// Check if contract address already exists
const auto derivedContractAddress = this->deriveContractAddress(callInfo);
if (this->contracts.contains(derivedContractAddress)) {
throw std::runtime_error("Contract already exists");
}
â
std::unique_lock lock(this->contractsMutex);
for (const auto& [protocolContractName, protocolContractAddress] : ProtocolContractAddresses) {
if (protocolContractAddress == derivedContractAddress) {
throw std::runtime_error("Contract already exists");
}
}
â
// Parse the constructor ABI
std::vector<ABI::Types> types = { ABI::Types::string, ABI::Types::uint256 };
ABI::Decoder decoder(types, std::get<5>(callInfo).substr(4));
â
// Register the function
this->contracts.insert(std::make_pair(derivedContractAddress, std::make_unique<SimpleContract>(
decoder.getData<std::string>(0),
decoder.getData<uint256_t>(1),
this->interface,
derivedContractAddress,
this->getCaller(),
this->options->getChainID(),
this->db
)));
}
â
void ContractManager::validateCreateNewSimpleContractContract(const ethCallInfo& callInfo) const {
// Check if caller is creator
if (this->caller != this->getContractCreator()) {
throw std::runtime_error("Only contract creator can create new contracts");
}
â
// Check if contract address already exists
const auto derivedContractAddress = this->deriveContractAddress(callInfo);
if (this->contracts.contains(derivedContractAddress)) {
throw std::runtime_error("Contract already exists");
}
â
std::unique_lock lock(this->contractsMutex);
for (const auto &[protocolContractName, protocolContractAddress]: ProtocolContractAddresses) {
if (protocolContractAddress == derivedContractAddress) {
throw std::runtime_error("Contract already exists");
}
}
â
// Parse the constructor ABI
std::vector<ABI::Types> types = {ABI::Types::string, ABI::Types::uint256};
ABI::Decoder decoder(types, std::get<5>(callInfo).substr(4));
}
â
...more source code...
Both functions should follow the same structure - checking if the caller is the contract creator, checking if the contract already exists, and parsing the ABI to create the contract.
After that, we must register both functions within the ContractManager's
ethCall()
function. This function contains two if cases - one to truly commit to that function (where you should call the create function), and one that will only validate if the function doesn't throw (where you should call the validate function).We must calculate the function signature for our create function - in our case it's
keccak256("createNewSimpleContractContract(string,uint256)").substr(0,4)
(again, done with Utils::sha3()
instead), which equals 0x6de23252
.The
ethCall()
function should look something like this:void ContractManager::ethCall(const ethCallInfo& callInfo) {
std::string functor = std::get<5>(callInfo).substr(0, 4);
if (this->getCommit()) {
...some if blocks...
// function createNewSimpleContract(string memory name, uint256 supply) public {}
if (functor == Hex::toBytes("0x6de23252")) {
this->createNewSimpleContractContract(callInfo);
return;
}
...more if blocks...
} else {
...some if blocks...
if (functor == Hex::toBytes("0x6de23252")) {
this->validateCreateNewSimpleContractContract(callInfo);
return;
}
...more if blocks...
}
throw std::runtime_error("Invalid function call");
}
Finally, modify the
ContractManager
constructor to include loading a contract from the database. It should look something like this:ContractManager::ContractManager(
State* state, const std::unique_ptr<DB>& db,
const std::unique_ptr<rdPoS>& rdpos, const std::unique_ptr<Options>& options
) : state(state),
BaseContract("ContractManager", ProtocolContractAddresses.at("ContractManager"), Address(Hex::toBytes("0x00dead00665771855a34155f5e7405489df2c3c6"), true), 0, db),
rdpos(rdpos),
options(options),
interface(*this)
{
// Load Contracts from DB
auto contracts = this->db->getBatch(DBPrefix::contractManager);
for (const auto& contract : contracts) {
...some if blocks...
if (contract.value == "SimpleContract") {
Address contractAddress(contract.key, true);
this->contracts.insert(
std::make_pair(contractAddress, std::make_unique<SimpleContract>(this->interface, contractAddress, this->db)));
continue;
}
...more if blocks...
throw std::runtime_error("Unknown contract: " + contract.value);
}
}
At last, we can deploy the blockchain, and to do so, we must first compile the code, and then deploy it using
AIO-setup.sh
. See 3.3 for more information.We use the Catch2 framework to test our project as a whole, so it is possible to create an automated test using catch2 for your contract.
In order to do that, you must create a new file in
tests/contracts/
with the name of your contract - in our case, tests/contracts/simplecontract.cpp
:#include "../../src/libs/catch2/catch_amalgamated.hpp"
#include "../../src/contract/erc20.h"
#include "../../src/contract/abi.h"
#include "../../src/utils/db.h"
#include "../../src/utils/options.h"
#include "../../src/contract/contractmanager.h"
#include "../../src/core/rdpos.h"
â
#include <filesystem>
â
/// Forward Declaration.
ethCallInfo buildCallInfo(const Address& addressToCall, const std::string& dataToCall);
â
void initialize(
std::unique_ptr<Options>& options,
std::unique_ptr<DB>& db,
std::unique_ptr<ContractManager> &contractManager,
const std::string& dbName,
const PrivKey& ownerPrivKey,
const std::string& name,
const uint256_t& value,
bool deleteDB = true
) {
if (deleteDB) {
if (std::filesystem::exists(dbName)) {
std::filesystem::remove_all(dbName);
}
}
â
options = std::make_unique<Options>(Options::fromFile(dbName));
db = std::make_unique<DB>(dbName);
std::unique_ptr<rdPoS> rdpos;
contractManager = std::make_unique<ContractManager>(nullptr, db, rdpos, options);
â
if (deleteDB) {
/// Create the contract.
ABI::Encoder::EncVar createNewSimpleContractVars;
createNewSimpleContractVars.push_back(name);
createNewSimpleContractVars.push_back(value);
ABI::Encoder createNewSimpleContractEncoder(createNewSimpleContractVars);
std::string createNewSimpleContractData = Hex::toBytes("0x6de23252") + createNewSimpleContractEncoder.getRaw();
â
TxBlock createNewSimpleContractTx = TxBlock(
ProtocolContractAddresses.at("ContractManager"),
Secp256k1::toAddress(Secp256k1::toUPub(ownerPrivKey)),
createNewSimpleContractData,
8080,
0,
0,
0,
0,
0,
ownerPrivKey
);
â
contractManager->callContract(createNewSimpleContractTx);
}
}
â
namespace TSimpleContract {
TEST_CASE("Simple Contract class", "[contract][simplecontract]") {
PrivKey ownerPrivKey(Hex::toBytes("0xe89ef6409c467285bcae9f80ab1cfeb3487cfe61ab28fb7d36443e1daa0c2867"));
Address owner = Secp256k1::toAddress(Secp256k1::toUPub(ownerPrivKey));
...
}
}
set (TESTS_SOURCES
...
${CMAKE_SOURCE_DIR}/tests/contract/simplecontract.cpp
...
With this, we have a simple environment skeleton to test our contract, and we can now create a test to check if the contract was created.
To do so, we must create a new test case, and call the
initialize()
function, which will create the contract within the ContractManager.The
buildCallInfo() function
is a forward declaration for usage within a view function of your contract.The
initialize()
function mostly remains the same across all contracts, only changing the contract name, and the contract parameters.In order to test contract creation, we can add the following section to our tests:
namespace TSimpleContract {
TEST_CASE("SimpleContract class", "[contract][simplecontract]") {
PrivKey ownerPrivKey(Hex::toBytes("0xe89ef6409c467285bcae9f80ab1cfeb3487cfe61ab28fb7d36443e1daa0c2867"));
Address owner = Secp256k1::toAddress(Secp256k1::toUPub(ownerPrivKey));
SECTION("SimpleContract creation") {
Address contractAddress;
{
std::unique_ptr<Options> options;
std::unique_ptr<DB> db;
std::unique_ptr<ContractManager> contractManager;
initialize(options, db, contractManager, "SimpleContractCreationTest", ownerPrivKey, "TestName", 19283187581);
â
/// Get the contract address.
contractAddress = contractManager->getContracts()[0].second;
â
ABI::Encoder getNameEncoder({}, "getName()");
ABI::Encoder getValueEncoder({}, "getValue()");
â
std::string nameData = contractManager->callContract(buildCallInfo(contractAddress, getNameEncoder.getRaw()));
std::string valueData = contractManager->callContract(buildCallInfo(contractAddress, getValueEncoder.getRaw()));
â
ABI::Decoder nameDecoder({ABI::Types::string}, nameData);
ABI::Decoder valueDecoder({ABI::Types::uint256}, valueData);
â
REQUIRE(nameDecoder.getData<std::string>(0) == "TestName");
REQUIRE(valueDecoder.getData<uint256_t>(0) == 19283187581);
}
â
std::unique_ptr<Options> options;
std::unique_ptr<DB> db;
std::unique_ptr<ContractManager> contractManager;
initialize(options, db, contractManager, "SimpleContractCreationTest", ownerPrivKey, "TestName", 19283187581, false);
â
â
REQUIRE(contractAddress == contractManager->getContracts()[0].second);
â
ABI::Encoder getNameEncoder({}, "getName()");
ABI::Encoder getValueEncoder({}, "getValue()");
â
std::string nameData = contractManager->callContract(buildCallInfo(contractAddress, getNameEncoder.getRaw()));
std::string valueData = contractManager->callContract(buildCallInfo(contractAddress, getValueEncoder.getRaw()));
â
ABI::Decoder nameDecoder({ABI::Types::string}, nameData);
ABI::Decoder valueDecoder({ABI::Types::uint256}, valueData);
â
REQUIRE(nameDecoder.getData<std::string>(0) == "TestName");
REQUIRE(valueDecoder.getData<uint256_t>(0) == 19283187581);
}
}
}
Keep in mind that we only access the ContractManager and not the contract itself, which requires us to parse inputs and outputs from the ContractManager.
This test also includes checking if our database destructor is working properly, by creating a ContractManager and the contract within the manager, unloading it, and then loading it again, and checking if the contract is still there.
We can continue and test the remaining functions by calling ContractManager with a transaction:
SECTION("SimpleContract setName and setValue") {
Address contractAddress;
{
std::unique_ptr<Options> options;
std::unique_ptr<DB> db;
std::unique_ptr<ContractManager> contractManager;
initialize(options, db, contractManager, "SimpleContractSetNameAndSetValue", ownerPrivKey, "TestName", 19283187581);
â
/// Get the contract address.
contractAddress = contractManager->getContracts()[0].second;
â
ABI::Encoder getNameEncoder({}, "getName()");
ABI::Encoder getValueEncoder({}, "getValue()");
â
std::string nameData = contractManager->callContract(buildCallInfo(contractAddress, getNameEncoder.getRaw()));
std::string valueData = contractManager->callContract(buildCallInfo(contractAddress, getValueEncoder.getRaw()));
â
ABI::Decoder nameDecoder({ABI::Types::string}, nameData);
ABI::Decoder valueDecoder({ABI::Types::uint256}, valueData);
â
ABI::Encoder setNameEncoder({"TryThisName"}, "setName(string)");
ABI::Encoder setValueEncoder({uint256_t("918258172319061203818967178162134821351")}, "setValue(uint256)");
â
TxBlock setNameTx(
contractAddress,
owner,
setNameEncoder.getRaw(),
8080,
0,
0,
0,
0,
0,
ownerPrivKey
);
â
TxBlock setValueTx(
contractAddress,
owner,
setValueEncoder.getRaw(),
8080,
0,
0,
0,
0,
0,
ownerPrivKey
);
â
contractManager->callContract(setNameTx);
contractManager->callContract(setValueTx);
â
nameData = contractManager->callContract(buildCallInfo(contractAddress, getNameEncoder.getRaw()));
valueData = contractManager->callContract(buildCallInfo(contractAddress, getValueEncoder.getRaw()));
â
nameDecoder = ABI::Decoder({ABI::Types::string}, nameData);
valueDecoder = ABI::Decoder({ABI::Types::uint256}, valueData);
â
REQUIRE(nameDecoder.getData<std::string>(0) == "TryThisName");
REQUIRE(valueDecoder.getData<uint256_t>(0) == uint256_t("918258172319061203818967178162134821351"));
}
â
std::unique_ptr<Options> options;
std::unique_ptr<DB> db;
std::unique_ptr<ContractManager> contractManager;
initialize(options, db, contractManager, "SimpleContractSetNameAndSetValue", ownerPrivKey, "TestName", 19283187581, false);
â
â
REQUIRE(contractAddress == contractManager->getContracts()[0].second);
â
ABI::Encoder getNameEncoder({}, "getName()");
ABI::Encoder getValueEncoder({}, "getValue()");
â
std::string nameData = contractManager->callContract(buildCallInfo(contractAddress, getNameEncoder.getRaw()));
std::string valueData = contractManager->callContract(buildCallInfo(contractAddress, getValueEncoder.getRaw()));
â
ABI::Decoder nameDecoder({ABI::Types::string}, nameData);
ABI::Decoder valueDecoder({ABI::Types::uint256}, valueData);
â
REQUIRE(nameDecoder.getData<std::string>(0) == "TryThisName");
REQUIRE(valueDecoder.getData<uint256_t>(0) == uint256_t("918258172319061203818967178162134821351"));
}
SECTIONS()
should always be placed within TEST_CASE()
scope.# Enter the build directory.
cd build_local_testnet
# Build - pick one of the lines
make -j$(nproc)
cmake --build . -- -j$(nproc)
# Run the tests
./orbitersdkd-tests [simplecontract] -d yes
The
[simplecontract]
label forces only the tests for the contract to run. This is set in the TEST_CASE()
line in the examples above.