A basic token generation contract in Etch

We are going to create a digital token using Etch, the language used to generate bytecode for the Fetch Virtual Machine.


This is a basic introduction and explanation on creating a FET contract on Fetch.AI, written in Fetch.AI's VM language Etch. We compile the contract with our Python ledger API. A compliant FET token contract can be quite complicated, but for this reference we will guide you through creating our minimum viable implementation of a token contract.

Basic contract

The contract will be comprised of three components:

  1. Initialisation of the contract
  2. A token transfer mechanism
  3. A balance query function

In the subsequent sections we will describe the code of each the these components in detail. The initialisation contract will be responsible for creating the tokens in the Fetch.AI state database, the transfer mechanism will allow us to transfer tokens from one address to the other and the balance query ensures that we can get the balance of our account at any given time.

Init

First, we need to create an init function. This is the first thing that will be executed.

@init
function initialise(owner: Address)
    var INITIAL_SUPPLY = 100000000000ul;
    var account = State<UInt64>(owner, 0ul);
    account.set(INITIAL_SUPPLY);
endfunction

In Etch, an init function is indicated by an @init tag. This 'decorator' tells the compiler that this function should be called just once on contract initialisation.

Any contract in Etch can have an initialise() function, though its name can be anything as long as it is annotated with@init. Upon compilation, the owner of the contract will have the tokens sent to their address. We pass the owner of type Address as a parameter of the initialise() function. In Etch a variable as a parameter is followed by its type delimited by a colon.

Once we've defined the init function, we set the owner of the contract, and give the contract some details:

var INTIITAL_SUPPLY = 987654321987654321000000000ul;

INITITAL_SUPPLY is a hardcoded token amount, this could be passed as a parameter too, if you wanted.

These are the only two values necessary to create a token on Fetch.AI, on compilation the contract will have an owner and its initial supply set.

Now, we're starting to get into the core syntax of Etch, and how we handle variable storage and type:

var account = State<UInt64>(owner, 0ul);
account.set(INTIITAL_SUPPLY);

With var account we're setting a variable account, account is a variable referencing a persistent State. This State is stored outside of the contract, on the node(s).

State<UInt64>("balances");

Here, we tell the compiler that we're accessing a Persistent State, in this instance a Map. This has a key of the OWNER_PUB_KEY

If not already constructed we pass 0ul, a 0 valued unsigned long.

Now we have a reference to the Persistent Map, we can then set the balance as account.set(INTIITAL_SUPPLY);.

Transfer

Now, we need the logic to be able to transfer a token, from one address to another.

@action
function transfer(from: Address, to: Address, amount: UInt64)
  // define the accounts
  var from_account = State<UInt64>(from, 0ul);
  var to_account = State<UInt64>(to, 0ul); // if new sets to 0u
  // Check if the sender has enough balance to proceed
  if (from_account.get() >= amount)
    from_account.set(from_account.get() - amount);
    to_account.set(to_account.get() + amount);
  endif
endfunction

There is a new tag for the compiler @action, this tells the compiler that the function that follows has a cost applied; one that you will pay when you call this function on the contract (to pay for the compute).

No token contract on Fetch.AI can be created without a transfer function, not one that is usable anyway:

function transfer(from : Address, to : Address, amount : Int32) : Bool

Then we get a reference to the from and to addresses.

var from_account = State<UInt64>(from, 0ul);
var to_account = State<UInt64>(to, 0ul);

Again, we're initialising references to States, they are keys of type Int32 names from and to, with a default value of 0ul if not already constructed.

Next, we make sure the from account has enough tokens in their account to transact, if it does we're going to set some balances:

Next, we make sure the from account has enough tokens in their account to transact, and set the new balance for the from and to addresses.

if (from_account.get() >= amount)
       from_account.set(from_account.get() - amount);
       to_account.set(to_account.get() + amount);
   endif
endfunction

.get() and .set() are functions of the object Address, these are generated by the base class.

Balance

Finally, we create a balance() function. The balance() function is wrapped by a new tag @query. @query are free functions to call on a contract, requiring no charge.

@query
function balance(address : Address) : UInt64
	var account = State<UInt64>(address, 0ul);
	return account.get();
endfunction

balance() takes an argument of a type Address named address; balance() returns an Uint64 - an Address.

The body of balance() doesn't introduce anything we haven't discussed previously.

Compilation

We currently compile the contracts using our python.ledger.api. You can read how to install and setup this here.

Compilation is quite straight forward, but there are prerequisites:

  • Mac OS/ Linux machine
  • Python Ledger installed on the machine, or access to the testnet
  • Python Ledger API installed on machine (guide here)

With all that done, I'm going to share a larger Python3 script which builds and sends our contract to your local node:

// contract.etch

@init
function initialise()
	var INTIITAL_SUPPLY = 987654321987654321000000000ul;
	var OWNER_PUB_KEY = Address("32423423423423987");

	var account = State<UInt64>(OWNER_PUB_KEY, 0ul);
	account.set(INTIITAL_SUPPLY);
endfunction

@action
function transfer(from : Address, to : Address, amount : UInt64) : Bool
  // define the accounts
  var from_account = State<UInt64>(from, 0ul);
  var to_account = State<UInt64>(to, 0ul); // if new sets to 0u

  	if (from_account.get() >= amount)          // Check if the sender has enough
    	from_account.set(from_account.get() - amount);
    	to_account.set(to_account.get() + amount);
    endif

endfunction

@query
function balance(address : Address) : UInt64
	var account = State<UInt64>(address, 0ul);
	return account.get();
endfunction
contract_source = """
  @init
  function initialise()
    var INTIITAL_SUPPLY = 987654321987654321000000000ul;
    var OWNER_PUB_KEY = Address("32423423423423987");
    var account = State<UInt64>(OWNER_PUB_KEY, 0ul);
    account.set(INTIITAL_SUPPLY);
  endfunction

  @action
  function transfer(from : Address, to : Address, amount : UInt64) : Bool
    // define the accounts
    var from_account = State<UInt64>(from, 0ul);
    var to_account = State<UInt64>(to, 0ul); // if new sets to 0u
    if (from_account.get() >= amount)          // Check if the sender has enough
      from_account.set(from_account.get() - amount);
      to_account.set(to_account.get() + amount);
    endif
  endfunction

  @query
  function balance(address : Address) : UInt64
    var account = State<UInt64>(address, 0ul);
    return account.get();
  endfunction

"""

import base64
import time
import hashlib
import json
import binascii
import msgpack

from fetchai.ledger.serialisation.objects.transaction_api import create_json_tx
from fetchai.ledger.api import ContractsApi, TransactionApi, submit_json_transaction
from fetchai.ledger.crypto import Identity

HOST = '127.0.0.1'
PORT = 8000


identity = Identity()
next_identity = Identity()

status_api = TransactionApi(HOST, PORT)
contract_api = ContractsApi(HOST, PORT)

create_tx = contract_api.create(identity, contract_source, init_resources = ["owner", identity.public_key])

print('CreateTX:', create_tx)

while True:
    status = status_api.status(create_tx)

    print(status)
    if status == "Executed":
        break

    time.sleep(1)

# re-calc the digest
hash_func = hashlib.sha256()
hash_func.update(contract_source.encode())
source_digest = base64.b64encode(hash_func.digest()).decode()

print('transfer N times')

for index in range(3):

    # create the tx
    tx = create_json_tx(
        contract_name=source_digest + '.' + identity.public_key + '.transfer',
        json_data=msgpack.packb([msgpack.ExtType(77, identity.public_key_bytes), msgpack.ExtType(77, next_identity.public_key_bytes), 1000 + index]),
        resources=['owner', identity.public_key, next_identity.public_key],
        raw_resources=[source_digest],
    )

    # sign the transaction contents
    tx.sign(identity.signing_key)

    wire_fmt = json.loads(tx.to_wire_format())
    print(wire_fmt)

    # # submit that transaction
    code = submit_json_transaction(HOST, PORT, wire_fmt)

    print(code)

    time.sleep(5)

print('Query for owner funds')

source_digest_hex = binascii.hexlify(base64.b64decode(source_digest)).decode()

url = 'http://{}:{}/api/contract/{}/{}/owner_funds'.format(HOST, PORT, source_digest_hex, identity.public_key_hex)

print(url)

r = status_api._session.post(url, json={})
print(r.status_code)
print(r.json())

Our default token contract build and deployment script is aimed to give you quick feedback on contract deployment success. It has a loop which send all funds to another account, testing transfer and balance, and this can be seen in the output when the script is run.

We briefly introduce the fetch.ledger.api here , and we are creating a far greater introduction which will be available soon.

Address

St. John's Innovation Centre,
Cowley Road,
Cambridge,
CB4 0WS
UK

Go to map
Technical: [email protected]
Investors: [email protected]