Technical Guide | Teach you to discuss Wasm contract development: (C++)

Ontology Wasm has received a lot of attention from community developers since its launch . The launch of Wasm will reduce the cost of dApp contracting for complex business logic and greatly enrich the dApp ecosystem. When developing Wasm contracts, developers can use not only Rust, but also C++ as a contract development language. In this issue we will demonstrate how to use C++ for Wasm contract development with two simple examples.

First, Hello World

By convention, we still start with a 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 developers do not need to redefine the entry method. The next step is to define the external interface of the contract, which is the way the smart contract provides services.
  ONTIO_DISPATCH(hello, (sayHello)); 

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

  Printf("hello world!"); 

This "Hello world!" will be printed in the node's log with debug information. In practical applications, printf can only be used for debugging purposes, an actual smart contract that requires more complex functions.

1.2 Smart Contract API

Ontology Wasm provides the following API to interact with the underlying layer of the blockchain:

Second, the red envelope contract

Let's take a more complex example to demonstrate how to develop a complete Wasm smart contract through these APIs.

In many cases, we will make red packets through various apps, such as WeChat and other chat tools. We can send a red envelope to a friend, or you can grab a red envelope sent by someone else, and the money received will be credited to your personal WeChat account.

Similar to WeChat's process, we will try to create a smart contract. Users can use this contract to send ONT, ONG or standard OEP-4 Token asset red packets to his friends, while the red packets that friends grab can be directly transferred to their wallet accounts.

2.1 Creating a contract

First, we need to create a new contract source file, temporarily named redEnvelope.cpp. We need three interfaces for this contract:

  • createRedEnvelope: create a red envelope
  • queryEnvelope: Query red envelope information
  • claimEnvelope: grab red envelope
  #include<ontiolib/ontio.hpp>
 Using namespace ontio;
 Class redEnvelope: public contract{
 }
 ONTIO_DISPATCH(redEnvelope, (createRedEnvelope)(queryEnvelope)(claimEnvelope)) 
We need to save some key data in the store. In smart contracts, data is stored in the context of the contract in the form of KVs, and the KEY of these data needs to be prefixed for later queries. Three different prefixes are defined below for use:
  Std::string rePrefix = "RE_PREFIX_";
 Std::string sentPrefix = "SENT_COUNT_";
 Std::string claimPrefix = "CLAIM_PREFIX_"; 

Because our contract supports the native assets of ONT and ONG, we can pre-define the contract addresses of these two assets. Unlike standard smart contracts, the contractual address of the Ontology native contract is fixed, not based on the hash calculation 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 red envelope information in the contract, such as the red envelope's asset information (the contract address of the token, the total amount of the red envelope, the number of red envelopes, etc.).
  Struct receiveRecord{
 Address account; //user address asset amount; //the amount of the rushed ONTLIB_SERIALIZE(receiveRecord,(account)(amount))
 };
 Struct EnvelopeStruct{
 Address tokenAddress; / / asset token address asset totalAmount; / / red packet total amount asset totalPackageCount; / / red packets total assets remainAmount; / / current remaining amount of assets remainPackageCount; / / current remaining red packets number std:: vector <struct receiveRecord > records; //The record that has been robbed ONTLIB_SERIALIZE( EnvelopeStruct, (tokenAddress)(totalAmount)(totalPackageCount)(remainAmount)(remainPackageCount)(records) )
 }; 

among them,
  ONTLIB_SERIALIZE(receiveRecord,(account)(amount)) 

Is a macro operation defined by the Ontology Wasm CDT that is used to serialize the struct before it is stored.

2.2 Creating a red envelope

Preparation is almost done, let's start developing specific interface logic.
1. To create a red envelope, you need to specify the creator address, the number of red packets, the red envelope amount, and the contract address of the asset:
  Bool createRedEnvelope(address owner,asses packcount, asset amount,address tokenAddr ){
 Return true;
 } 

2. Check if there is a creator's signature, otherwise the transaction rolls back and exits:
  Ontio_assert(check_witness(owner),"checkwitness failed"); 

NOTE: ontio_assert(expr, errormsg): When expr is false, an exception is thrown and exits.
3. If the red envelope asset is an ONT, due to the indivisibility of the ONT (minimum 1 ONT), the amount of the red envelope should be greater than or equal to the number of red envelopes, and ensure that each red envelope has at least 1 ONT:
  If (isONTToken(tokenAddr)){
 Ontio_assert(amount >= packcount,"ont amount should greater than packcount");
 } 

4. For each creator of the red envelope, we need to record the total number of red envelopes he sent:
  Key sentkey = make_key(sentPrefix,owner.tohexstring());
 Asset sentcount = 0;
 Storage_get(sentkey,sentcount);
 Sentcount += 1;
 Storage_put(sentkey,sentcount); 

5. Generate a red envelope hash, which is the unique ID that identifies the red envelope:
  H256 hash ;
 Hash256(make_key(owner,sentcount),hash);
 Key rekey = make_key(rePrefix,hash256ToHexstring(hash)); 

6. According to the type of the token asset, the asset is transferred to the contract, self_address() can get the current executed contract address, and we transfer the specified number of tokens to the contract according to the type of token input by the user:
  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 the two native assets, ONT and ONG, Ontology Wasm CDT provides the ont::transfer API for transfer operations; while OEP-4 assets need to be transferred according to the normal cross-contract call method.

NOTE 2 : Like the normal wallet address, the contract address can accept any type of asset. But the contract address is generated by the binary code hash compiled by the contract, so there is no corresponding private key, and you can't manipulate the assets in the contract at will. If you don't set the operation on the asset in the contract , it means you will Unable to control this part of the asset.

7. Save the contract information in the store:

  Struct EnvelopeStruct es ;
 es.tokenAddress = tokenAddr;
 es.totalAmount = amount;
 es.totalPackageCount = packcount;
 es.remainAmount = amount;
 es.remainPackageCount = packcount;
 Es.records = {};
 Storage_put(rekey, es); 
8. Send an event to create a red envelope. The call to the smart contract is an asynchronous process, the contract will send an event to notify the client to execute the result after the execution is successful. The format of the event can be specified by the contract writer.
  Char buffer [100];
 Sprintf(buffer, "{\"states\":[\"%s\", \"%s\", \"%s\"]}","createEnvelope",owner.tohexstring().c_str() ,hash256ToHexstring(hash).c_str());
 Notify(buffer);
 Return true; 

A simple red envelope is created. Next we need to implement information on how to query this red envelope.

2.3 Querying Red Packets

The logic for querying the red envelope is very simple. You only need to take out the red envelope information in the storage and format it back:
  Std::string queryEnvelope(std::string hash){
 Key rekey = make_key(rePrefix, hash);
 Struct EnvelopeStruct es;
 Storage_get(rekey, es);
 Return formatEnvelope(es);
 } 

NOTE: For read-only operations (such as queries) on smart contracts, the results can be read by pre-exec. Unlike ordinary contract calls, pre-execution does not require the signature of the wallet, and there is no need to spend ONG. Finally, other users can pick up (grab) the red envelope based on the hash (the ID of the red envelope).

2.4 Receiving a red envelope

We have successfully transferred the asset to the smart contract, and then we can send the ID of the red envelope to your friends to get the red envelope.

1. To receive the red envelope, you need to enter the recipient's account and red envelope hash:

  Bool claimEnvelope(address account, std::string hash){
 Return true;
 } 
2. Similarly, we need to verify the signature of the account, do not allow red packets for others, and only rede each red envelope per account:
  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 Envelope!"); 

3. According to the hash, take out the red envelope information from the storage and judge whether the red envelope has not been robbed:
  Key rekey = make_key(rePrefix,hash);
 Struct EnvelopeStruct es;
 Storage_get(rekey,es);
 Ontio_assert(es.remainAmount > 0, "the Envelope has been claimed over!");
 Ontio_assert(es.remainPackageCount > 0, "the Envelope has been claimed over!"); 

4. Create a new collection of records:
  Struct receiveRecord record ;
 Record.account = account;
 Asset claimAmount = 0; 

5. Calculate the number of assets that receive the red envelope this time. If it is the last red envelope, the quantity is the remaining amount. Otherwise, the random number is calculated according to the current block hash, the quantity received this time is determined, 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. According to the calculation results, transfer the corresponding assets from the contract to the received account:
  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 received information, write the updated red envelope information back to the store and send a notification event:
  Storage_put(claimkey,claimAmount);
 Storage_put(rekey,es);
 Char buffer [100];
 Std::sprintf(buffer, "{\"states\":[\"%s\",\"%s\",\"%s\",\"%lld\"]}","claimEnvelope" , hash.c_str(), account.tohexstring().c_str(), claimAmount);
 Notify(buffer);
 Return true; 

As mentioned earlier, this contract can only transfer assets out of the contract via the claimEnvelope interface. Therefore, the assets in the contract are safe, and no one can freely take away the assets inside. At this point, a simple red envelope contract is logically completed. The complete contract code is as follows: https://github.com/JasonZhouPW/pubdocs/blob/master/redEnvelope.cpp

2.5 contract test

There are two ways to test a contract:

  1. Use the CLI

Please refer to: https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/How_To_Run_ontologywasm_node.md

  • Use the Golang SDK
  • Please refer to: https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/example/other/main.go

    Third, summary

    This example is just to show how to write a complete Ontology Wasm smart contract, how to interact with the underlying blockchain by calling the API. If you want to be a formal product, you need to solve the privacy problem of the red envelope: Everyone can get the red envelope hash by monitoring the contract event, which means everyone can grab the red envelope. A simpler solution is to specify which accounts can be picked up when creating a red envelope. If you are interested, you can also try to modify the test.

    We welcome more Wasm technology enthusiasts to join the ontology development community to create a technological ecosystem.