How to Develop Wasm Contract with C ++ on Ontology

The Ontology Team
OntologyNetwork
Published in
8 min readSep 3, 2019

Since the Ontology Wasm was launched on the Ontology TestNet, it has received wide attention from community developers as this technology reduces the cost of moving dApp contracts with complex business logic onto the blockchain, thus greatly enriching the dApp ecosystem.

In a previous article, we introduced how you can develop Wasm contract with Rust. In this article, we will give two examples to show you how to develop Wasm contract with C++ on Ontology.

1. Hello World

Let’s start with Hello World:

#include<ontiolib/ontio.hpp>
#include<stdio.h>

using namespace ontio;
class hello:public contract {
public:
using contract::contract:
void sayHello(){
printf("hello world!");
}
};
ONTIO_DISPATCH(hello, (sayHello));

1.1 Contract Entry

The Ontology Wasm CDT compiler has encapsulated the entry and parameter parsing so that developers do not need to redefine entry methods. Then developers need to define the API of the contract, which is the method of how smart contract provides services.

ONTIO_DISPATCH(hello, (sayHello));

In the example above, we only support sayHello method for the moment:

printf("hello world!");

Hello World!” will be printed out as debug information in the node log. In real-life use, printf can only be used for debugging while a true smart contract needs to realize more complex functions.

1.2 Smart Contract API

Ontology Wasm provides the following API to interact with the blockchain back-end:

2. Red Envelope Contract

Now let’s take a look at a more complex example to see how to use these APIs to develop a complete Wasm smart contract.

For example, we often use some apps, such as WeChat, to send money to our friends via virtual red envelopes or receive money from them. The money you received will be added to your WeChat account.

Now let’s create a smart contract similar to this. Users can use this contract to send ONT, ONG, or OEP-4 assets via virtual red envelopes to their friends, who can then transfer the assets into their wallet accounts.

2.1 Create Contract

First, we need to create the source file of the contract. Let’s name it redEnvelope.cpp for now. We need three APIs for this contract:

  • createRedEnvelope: Create red envelope
  • queryEnvelope: Query red envelope information
  • claimEnvelope: Claim red envelope
#include<ontiolib/ontio.hpp>

using namespace ontio;

class redEnvlope: public contract{

}
ONTIO_DISPATCH(redEnvlope, (createRedEnvlope)(queryEnvlope)(claimEnvelope));

Now we need to store some key data in the storage. In the smart contract, data is saved in the context of the contract in the form of KV and we need to add a prefix to the KEY of the data for later query. We have defined three prefixes below for your use:

std::string rePrefix = "RE_PREFIX_";
std::string sentPrefix = "SENT_COUNT_";
std::string claimPrefix = "CLAIM_PREFIX_";

Since the contract supports both ONT and ONG, two of Ontology’s native assets, we can predefine their contract address. Unlike a standard smart contract, the contract address of the Ontology native contract is fixed and does not derive from the hash of the contract code.

address ONTAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
address ONGAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2};

We need to save the information about the red envelope in the contract, such as asset information about the red envelope (token contract address, the total amount of the red envelope, and the number of red envelopes).

struct receiveRecord{
address account; //User address
asset amount; //Received amount
ONTLIB_SERIALIZE(receiveRecord,(account)(amount))
};

struct envlopeStruct{
address tokenAddress; //Token asset address
asset totalAmount; //Total amount
asset totalPackageCount; //Total number of red envelope
asset remainAmount; //Remaining amount
asset remainPackageCount; //Remaining number of red envelope
std::vector<struct receiveRecord> records; //Received records
ONTLIB_SERIALIZE( envlopeStruct, (tokenAddress)(totalAmount)(totalPackageCount)(remainAmount)(remainPackageCount)(records) )
};

Please note that below is the macro operation defined by Ontology Wasm CDT and used for serialization prior to struct storage.

ONTLIB_SERIALIZE(receiveRecord,(account)(amount))

2.2 Create Red Envelope

Now we have done the preparation work, let’s start developing the API logic.

1. When you create a red envelope, you need to specify the address of the owner, the number and amount of red envelopes, and asset contract address:

bool createRedEnvlope(address owner,asset packcount, asset amount,address tokenAddr ){

return true;
}

2. Check if it has the signature of the owner, otherwise roll back transaction and exit:

ontio_assert(check_witness(owner),"checkwitness failed");

NOTE: ontio_assert(expr, errormsg): when expr is false, error is returned and exited.

3. If the asset in the red envelope is ONT, since ONT is indivisible (the minimum is 1 ONT), the amount of the red envelope must be larger or equal its number to ensure there is at least 1 ONT in each red envelope:

if (isONTToken(tokenAddr)){
ontio_assert(amount >= packcount,"ont amount should greater than packcount");
}

4. For the owner of each red envelope, we need to record the total number of red envelopes he sends out:

key sentkey = make_key(sentPrefix,owner.tohexstring());
asset sentcount = 0;
storage_get(sentkey,sentcount);
sentcount += 1;
storage_put(sentkey,sentcount);

5. Red envelope hash is generated, which is the only ID that labels this red envelope:

H256 hash ;

hash256(make_key(owner,sentcount),hash) ;

key rekey = make_key(rePrefix,hash256ToHexstring(hash));

6. Transfer the assets into the contract based on the asset types. self_address() can get the address of the contract that is being executed, we then transfer a designated token amount into the contract based on the type of token users entered:

address selfaddr = self_address();
if (isONTToken(tokenAddr)){

bool result = ont::transfer(owner,selfaddr ,amount);
ontio_assert(result,"transfer native token failed!");

}else if (isONGToken(tokenAddr)){

bool result = ong::transfer(owner,selfaddr ,amount);
ontio_assert(result,"transfer native token failed!");
}else{
std::vector<char> params = pack(std::string("transfer"),owner,selfaddr,amount);
bool res;
call_contract(tokenAddr,params, res );

ontio_assert(res,"transfer oep4 token failed!");
}

NOTE 1: for ONT and ONG, Ontology Wasm CDT provides ont::transfer API for asset transfer; for OEP-4 assets, you need to transfer using the regular cross-contract call method.

NOTE 2: Similar to a regular wallet address, the contract address can accept any type of assets. However, the contract address is generated by the compiled binary code hash and thus does not have a corresponding private key and is unable to access the assets in the contract. If you have not set the method of accessing the assets, then you will not be able to control these assets.

7. Save contract information in the storage:

struct envlopeStruct es ;
es.tokenAddress = tokenAddr;
es.totalAmount = amount;
es.totalPackageCount = packcount;
es.remainAmount = amount;
es.remainPackageCount = packcount;
es.records = {};
storage_put(rekey, es);

8. Send the event of red envelope creation. This is an asynchronous process for calling the smart contract and the contract will send an event to notify the client of the execution result. The format of the event can be determined by the contract writer.

char buffer [100];
sprintf(buffer, "{\"states\":[\"%s\", \"%s\", \"%s\"]}","createEnvlope",owner.tohexstring().c_str(),hash256ToHexstring(hash).c_str());
notify(buffer);
return true;

And voila, a red envelope is created. Now let’s see how to query red envelope information.

2.3 Query Red Envelope

The logic of the query is quite simple, you only need to get the information from the storage, format it, and then return:

std::string queryEnvlope(std::string hash){
key rekey = make_key(rePrefix,hash);
struct envlopeStruct es;
storage_get(rekey,es);
return formatEnvlope(es);
}

NOTE: For read-only operations of the smart contract (e.g. query), you can read the result through pre-exec. Unlike regular contract call, pre-exec does not require a wallet signature and thus does not cost ONG. Having done this, other users can now claim the red envelope with the hash (red envelope ID).

2.4 Claim Red Envelope

Now we have successfully transferred the assets into the smart contract, you can then send the ID to your friends and ask them to claim the red envelope.

1. To claim the red envelope, you need to enter your account and red envelope hash:

bool claimEnvlope(address account, std::string hash){
return true;
}

2. Likewise, the signature of your account will be checked to ensure you are the owner of the account and each account can only claim once:

ontio_assert(check_witness(account),"checkwitness failed");
key claimkey = make_key(claimPrefix,hash,account);
asset claimed = 0 ;
storage_get(claimkey,claimed);
ontio_assert(claimed == 0,"you have claimed this envlope!");

3. Check if the red envelope has been claimed according to the information the hash obtained from the storage:

key rekey = make_key(rePrefix,hash);
struct envlopeStruct es;
storage_get(rekey,es);
ontio_assert(es.remainAmount > 0, "the envlope has been claimed over!");
ontio_assert(es.remainPackageCount > 0, "the envlope has been claimed over!");

4. Create a claim record:

struct receiveRecord record ;
record.account = account;
asset claimAmount = 0;

5. Calculate the claimed amount. If this is the last envelope, then the number is the remaining amount, otherwise, the claimed amount is determined by the random number calculated by the hash of the current block and the red envelope information is updated:

if (es.remainPackageCount == 1){
claimAmount = es.remainAmount;
record.amount = claimAmount;
}else{
H256 random = current_blockhash() ;
char part[8];
memcpy(part,&random,8);
uint64_t random_num = *(uint64_t*)part;
uint32_t percent = random_num % 100 + 1;
claimAmount = es.remainAmount * percent / 100;
//ont case
if (claimAmount == 0){
claimAmount = 1;
}else if(isONTToken(es.tokenAddress)){
if ( (es.remainAmount - claimAmount) < (es.remainPackageCount - 1)){
claimAmount = es.remainAmount - es.remainPackageCount + 1;
}
}
record.amount = claimAmount;
}
es.remainAmount -= claimAmount;
es.remainPackageCount -= 1;
es.records.push_back(record);

6. A corresponding amount of assets is transferred to the account that has claimed the red envelope according to the calculation result:

address selfaddr = self_address();
if (isONTToken(es.tokenAddress)){
bool result = ont::transfer(selfaddr,account ,claimAmount);
ontio_assert(result,"transfer ont token failed!");
} else if (isONGToken(es.tokenAddress)){
bool result = ong::transfer(selfaddr,account ,claimAmount);
ontio_assert(result,"transfer ong token failed!");
} else{
std::vector<char> params = pack(std::string("transfer"),selfaddr,account,claimAmount);
bool res = false;
call_contract(es.tokenAddress,params, res );
ontio_assert(res,"transfer oep4 token failed!");
}

7. Record the claim information and write the updated red envelope information back to the storage and send the notification event:

storage_put(claimkey,claimAmount);
storage_put(rekey,es);
char buffer [100];
std::sprintf(buffer, "{\"states\":[\"%s\",\"%s\",\"%s\",\"%lld\"]}","claimEnvlope",hash.c_str(),account.tohexstring().c_str(),claimAmount);

notify(buffer);
return true;

As is mentioned above, this contract can only transfer the assets out of the contract through claimEnvelope API. Therefore, the assets in the contract are safe since no one can withdraw the assets without certain requirements. And this is a simple red envelope contract logic, you can find the complete contract code here.

2.5 Contract Test

There are two ways to test the contract:

1. Use CLI

You can find it here

2. Use Golang SDK

You can find it here

3. Conclusion

In this article, we introduced how you can write a complete Ontology Wasm smart contract and call API to interact with the blockchain back-end. We still need to solve the privacy issue if we want to make it a product on the market since anyone can obtain the red envelope hash by monitoring the contract event, which means anyone can claim the red envelope. A simple solution to this issue is designating which accounts can claim the red envelope when creating it. If you are interested, you can also test it.

As a leading public blockchain, Ontology is among the first to support Wasm contract. We will continue to contribute to the Wasm technology and welcome more Wasm enthusiasts to join our developer community.

Are you a developer? Make sure you have joined our tech community on Discord. Also, take a look at the Developer Center on our website, there you can find developer tools, documentation, and more.

--

--