Skip to main content

Writing a contract

This tutorial explains how to create smart contracts that take full advantage of the unique features of =nil;.

Prerequisites

Complete the following tutorials before proceeding.

Specification

The tutorial defines two contracts.

Contract 1 is a retailer. The contract can call a special 'manufacturer' contract and order new products by performing an async call.

Contract 2 is the manufacturer. It has the following features:

Creating the first contract

The Retailer contract (Contract 1) orders products by performing an async call to the specified address of the manufacturer contract:

pragma solidity ^0.8.0;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Retailer {
using Nil for address;

receive() external payable {}

function orderProduct(address dst, string calldata name) public {
dst.asyncCall(
msg.sender,
msg.sender,
1_000_000,
Nil.FORWARD_VALUE,
false,
0,
abi.encodeWithSignature("createProduct(string)", name)
);
}

function verifyExternal(
uint256 hash,
bytes memory _authData
) external view returns (bool) {
return true;
}
}

Creating the second contract

Contract 2 has some basic architecture in place representing products:

pragma solidity ^0.8.0;

import "./Nil.sol";

contract Manufacturer {
using Nil for address;

struct Product {
uint id;
string name;
}

mapping(uint => Product) public products;
uint public nextProductId;
}

Contract 1 stores its owner's pubkey, which is useful for verifying external messages. The contract also contains the address of the retailer contract. This is done so that only the retailer contract can order new products.

    bytes pubkey;
address retailerContractAddress;

constructor(
bytes memory _pubkey,
address _retailerContractAddress
) payable {
pubkey = _pubkey;
retailerContractAddress = _retailerContractAddress;
}

receive() external payable {}

The contract also conntains a simple mechanism for verifying external messages:

contract Manufacturer {

function verifyExternal(
uint256 hash,
bytes calldata signature
) external view returns (bool) {
return Nil.validateSignature(pubkey, hash, signature);
}

}

Last but not least, Contract 2 can produce new products (but only via an internal message sent from the retailer):

function createProduct(
string calldata productName
) public onlyInternal returns (bool) {
if (msg.sender == retailerContractAddress) {
products[nextProductId] = Product(nextProductId, productName);
nextProductId++;
return true;
}
return false;
}

Contract 2 can also output the products that have been ordered by the retailer:

function getProducts()
public
view
returns (uint[] memory, string[] memory)
{
uint[] memory ids = new uint[](nextProductId);
string[] memory names = new string[](nextProductId);

for (uint i = 0; i < nextProductId; i++) {
Product storage product = products[i];
ids[i] = product.id;
names[i] = product.name;
}

return (ids, names);
}

Here is the full code of Contract 2:

pragma solidity ^0.8.0;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Manufacturer is NilBase {
using Nil for address;

bytes pubkey;
address retailerContractAddress;

receive() external payable {}

constructor(
bytes memory pubkeyOne,
address _retailerContractAddress
) payable {
pubkey = pubkeyOne;
retailerContractAddress = _retailerContractAddress;
}

struct Product {
uint id;
string name;
}

mapping(uint => Product) public products;
uint public nextProductId;

function createProduct(
string calldata productName
) public onlyInternal returns (bool) {
if (msg.sender == retailerContractAddress) {
products[nextProductId] = Product(nextProductId, productName);
nextProductId++;
return true;
}
return false;
}

function verifyExternal(
uint256 hash,
bytes calldata signature
) external view returns (bool) {
return Nil.validateSignature(pubkey, hash, signature);
}

function getProducts()
public
view
returns (uint[] memory, string[] memory)
{
uint[] memory ids = new uint[](nextProductId);
string[] memory names = new string[](nextProductId);

for (uint i = 0; i < nextProductId; i++) {
Product storage product = products[i];
ids[i] = product.id;
names[i] = product.name;
}

return (ids, names);
}
}

Compiling the contracts

Compile Contract 1 with:

solc -o path/to/Retailer --bin --abi path/to/Retailer.sol --overwrite 

Compile Contract 2 with:

solc -o path/to/Manufacturer --bin --abi path/to/Manufacturer.sol --overwrite 

After compiling both contracts, they should be ready for deployment!