SparqNet
Search
⌃K

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.

Solidity Example

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;
}
}

Step 1 - Creating the Files

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
...
)

Step 2 - Declaring the Contract Class

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.

Step 2.1 - Declaring the Contract Variables and Functions

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 a std::string with ABI encoded data
  • Non-view functions MUST be non-const and return void
  • 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
};

Step 2.2 - Declaring the Contract Constructor and Destructor

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.

Full Contract Header

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
You can also check the full header file here.

Step 3 - Defining the Contract Class

Now we can proceed to the definitions in the source file. Open src/contract/simplecontract.cpp and #include simplecontract.h right at the beginning.

Step 3.1 - Defining the Contract Constructors and Destructor

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() and updateState(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.

Step 3.2 - Defining the Contract Functions

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;
}

Step 3.3 - Registering the Contract Functions

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 function
  • registerViewFunction() 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));
});
}

Full Contract Source

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));
});
}
You can also check the full source file here.

Step 4 - Integrating the Contract within ContractManager

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

Step 4.1 - Implementing the Contract Creation 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.

Step 4.2 - Registering the Contract Functions inside ContractManager

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");
}

Step 4.3 - Loading the Contract within ContractManager

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);
}
}
You can see the full implementation here.

Step 5 - Deploying the Blockchain

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.

Step 6 (Optional) - Testing the Contract

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));
...
}
}
Add the file to tests/CMakeLists.txt by adding the following line:
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.
In order to run your tests, you can run the following command (see 3.3 for more information):
# 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.
You can find the full declaration for the test file here.