EOSIO Smart Contracts Tutorial
0. Required Background Knowledge
- C / C++ Experience
- Linux / Mac OS Experience
- Command Line Knowledge
1. Basics of EOSIO Smart Contract
1.1. Communication Model
EOSIO Smart Contracts communicate with each other in the form of actions and shared memory database access. There are two communication modes that can be defined within a contract:
- Inline. Inline is guaranteed to execute with the current transaction or unwind; no notification will be communicated regardless of success or failure. Inline operates with the same scopes and authorities the original transaction had.
- Deferred. Defer will get scheduled later at producer’s discretion; it’s possible to communicate the result of the communication or can simply timeout. Deferred can reach out to different scopes and carry the authority of the contract that sends them.
1.2. Action vs Transaction
A action represents a single operation, whereas a transaction is a collection of one or more actions. A contract and an account communicate in the form of actions. Actions can be sent individually, or in combined form if they are intended to be executed as a whole.
1.3. Transaction Confirmation
Receiving a transaction hash does not mean that the transaction has been confirmed, it only means that the node accepted it without error, which also means that there is a high probability other producers will accept it.
By means of confirmation, you should see the transaction in the transaction history with the block number of which it is included.
2. Starting a Private Blockchain
You can start your own single-node blockchain with this single command:
$ nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::account_history_api_plugin
Assuming everything worked properly, you should see a block generation message every 0.5 seconds.
This means your local blockchain is live, producing blocks, and ready to be used.
3. Creating a Wallet
A wallet is a repository of private keys necessary to authorize actions on the blockchain. These keys are stored on disk encrypted using a password generated for you. This password should be stored in a secure password manager.
$ cleos wallet create
Creating wallet: default
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"PW5JuBXoXJ8JHiCTXfXcYuJabjF9f9UNNqHJjqDVY7igVffe3pXub"
For the purpose of this simple development environment, your wallet is being managed by your local nodeos
via the eosio::wallet_api_plugin
we enabled when we started nodeos
. Any time you restart nodeos
you will have to unlock your wallet before you can use the keys within.
$ cleos wallet unlock --password PW5JuBXoXJ8JHiCTXfXcYuJabjF9f9UNNqHJjqDVY7igVffe3pXub
Unlocked: default
It is generally not secure to use your password directly on the commandline where it gets logged to your bash history, so you can also unlock in interactive mode:
$ cleos wallet unlock
password:
For security purposes it is generally best to leave your wallet locked when you are not using it. To lock your wallet without shutting down nodeos
you can do:
$ cleos wallet lock
Locked: default
You will need your wallet unlocked for the rest of this tutorial.
Import the master key for the eosio
account into your wallet. The master key can be found in the config.ini
file in the config folder for nodeos
. In this example, the default config folder is used. On MacOS, this will be in ~/Library/Application Support/eosio/nodeos/config
.
$ cleos wallet import 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
imported private key for: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
4. Loading the Bios Contract
The Bios contract enables you to have direct control over the resource allocation of other accounts and to access other privileged API calls. In a public blockchain, this contract will manage the staking and unstaking of tokens to reserve bandwidth for CPU and network activity, and memory for contracts.
$ cleos set contract eosio build/contracts/eosio.bios -p eosio
Reading WAST/WASM from build/contracts/eosio.bios/eosio.bios.wast...
Assembling WASM...
Publishing contract...
executed transaction: bb7fb236627de8ff4247064cd8b1bf8f266b9385491a94bd6ca26d26145fbe75 3288 bytes 2200576 cycles
# eosio <= eosio::setcode {"account":"eosio","vmtype":0,"vmversion":0,"code":"0061736d0100000001581060037f7e7f0060057f7e7e7e7e...
# eosio <= eosio::setabi {"account":"eosio","abi":{"types":[],"structs":[{"name":"set_account_limits","base":"","fields":[{"n...
The code defines how the contract runs and the abi describes how to convert between binary and json representations of the arguments. The result of this command sequence is that cleos
generated a transaction with two actions, eosio::setcode
and eosio::setabi
. It can be read as: The action setcode
as defined by eosio
was executed by eosio
contract with {args...}
The last argument to this call was -p eosio
. This tells cleos
to sign this action with the active authority of the eosio
account, i.e., to sign the action using the private key for the eosio
account that we imported earlier.
5. Creating Accounts
Now that we have setup the basic system contract, we can start to create our own accounts. We will create two accounts, user
and tester
, and we will need to associate a key with each account. In this example, the same key will be used for both accounts.
To do this we first generate a key for the accounts.
$ cleos create key
Private key: 5JxykKNaPZjhBETmfip8aDuP3UaZrCNBBznx17qFo4Sbyk7pE9Q
Public key: EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
Then we import this key into our wallet:
$ cleos wallet import 5JxykKNaPZjhBETmfip8aDuP3UaZrCNBBznx17qFo4Sbyk7pE9Q
imported private key for: EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
5.1. Create Two User Accounts
Next we will create two accounts, user
and tester
, using the key we created and imported above. The create account
subcommand requires two keys, one for the OwnerKey (which in a production environment should be kept highly secure) and one for the ActiveKey.
$ cleos create account eosio user EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
executed transaction: 8aedb926cc1ca31642ada8daf4350833c95cbe98b869230f44da76d70f6d6242 364 bytes 1000 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"user","owner":{"threshold":1,"keys":
$ cleos create account eosio tester EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
executed transaction: 414cf0dc7740d22474992779b2416b0eabdbc91522c16521307dd682051af083 366 bytes 1000 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"tester","owner":{"threshold":1,"keys":
We can query all accounts that are controlled by our key:
$ cleos get accounts EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
{
"account_names": [
"tester",
"user"
]
}
6. Deploy Token Contract
The eosio.token
contract enables the creation of many different tokens all running on the same contract but potentially managed by different users. Before we can deploy the token contract we must create an account to deploy it to.
$ cleos create account eosio eosio.token EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
Then we can deploy the contract which can be found in ${EOSIO_SOURCE}/build/contracts/eosio.token
$ cleos set contract eosio.token build/contracts/eosio.token -p eosio.token
Reading WAST...
Assembling WASM...
Publishing contract...
executed transaction: 528bdbce1181dc5fd72a24e4181e6587dace8ab43b2d7ac9b22b2017992a07ad 8708 bytes 10000 cycles
# eosio <= eosio::setcode {"account":"eosio.token","vmtype":0,"vmversion":0,"code":"0061736d0100000001ce011d60067f7e7f7f7f7f00...
# eosio <= eosio::setabi {"account":"eosio.token","abi":{"types":[],"structs":[{"name":"transfer","base":"","fields":[{"name"...
6.1. Create the EOS Token
You can view the interface to eosio.token
as defined by contracts/eosio.token/eosio.token.hpp
To create a new token we must call the create(...)
action with the proper arguments. This command will use the symbol of the maximum supply to uniquely identify this token from other tokens. The issuer will be the one with authority to call issue and or perform other actions such as freezing, recalling, and whitelisting of owners.
The concise way to call this method, using positional arguments:
$ cleos push action eosio.token create '[ "eosio", "1000000000.0000 EOS", 0, 0, 0]' -p eosio.token
executed transaction: 0e49a421f6e75f4c5e09dd738a02d3f51bd18a0cf31894f68d335cd70d9c0e12 260 bytes 1000 cycles
# eosio.token <= eosio.token::create {"issuer":"eosio","maximum_supply":"1000000000.0000 EOS","can_freeze":0,"can_recall":0,"can_whitelis...
Alternatively, a more verbose way to call this method, using named arguments:
$ cleos push action eosio.token create '{"issuer":"eosio", "maximum_supply":"1000000000.0000 EOS", "can_freeze":0, "can_recall":0, "can_whitelist":0}' -p eosio.token
executed transaction: 0e49a421f6e75f4c5e09dd738a02d3f51bd18a0cf31894f68d335cd70d9c0e12 260 bytes 1000 cycles
# eosio.token <= eosio.token::create {"issuer":"eosio","maximum_supply":"10
This command created a new token EOS
with a pecision of 4 decimials and a maximum supply of 1000000000.0000 EOS.
6.2. Issue Tokens to Account “User”
Now that we have created the token, the issuer can issue new tokens to the account user
we created earlier.
$ cleos push action eosio.token issue '[ "user", "100.0000 EOS", "memo" ]' -p eosio
executed transaction: 822a607a9196112831ecc2dc14ffb1722634f1749f3ac18b73ffacd41160b019 268 bytes 1000 cycles
# eosio.token <= eosio.token::issue {"to":"user","quantity":"100.0000 EOS","memo":"memo"}
>> issue
# eosio.token <= eosio.token::transfer {"from":"eosio","to":"user","quantity":"100.0000 EOS","memo":"memo"}
>> transfer
# eosio <= eosio.token::transfer {"from":"eosio","to":"user","quantity":"100.0000 EOS","memo":"memo"}
# user <= eosio.token::transfer {"from":"eosio","to":"user","quantity":"100.0000 EOS","memo":"memo"}
This time the output contains several different actions: one issue and three transfers. While the only action we signed was issue
, the issue
action performed an "inline transfer" and the "inline transfer" notified the sender and receiver accounts. The output indicates all of the action handlers that were called, the order they were called in, and whether or not any output was generated by the action.
Technically, the eosio.token
contract could have skipped the inline transfer
and opted to just modify the balances directly. However, in this case, the eosio.token
contract is following our token convention that requires that all account balances be derivable by the sum of the transfer actions that reference them. It also requires that the sender and receiver of funds be notified so they can automate handling deposits and withdrawals.
If you want to see the actual transaction that was broadcast, you can use the -d -j
options to indicate "don't broadcast" and "return transaction as json".
$ cleos push action eosio.token issue '["user", "100.0000 EOS", "memo"]' -p eosio -d -j
{
"expiration": "2018-04-25T23:50:50",
"region": 0,
"ref_block_num": 20835,
"ref_block_prefix": 3876493376,
"max_net_usage_words": 0,
"max_kcpu_usage": 0,
"delay_sec": 0,
"context_free_actions": [],
"actions": [{
"account": "eosio.token",
"name": "issue",
"authorization": [{
"actor": "eosio",
"permission": "active"
}
],
"data": "00000000007015d640420f000000000004454f5300000000046d656d6f"
}
],
"signatures": [
"EOSJwTiuUfr3iD4tTAeVYU8KP4GrV6jCyFiqPVZtWMKeisR7pr8moXYAaxhyf4VAhacPFea57eMKLbFCTaBxzBxqKiqLNBc9X"
],
"context_free_data": []
6.3. Transfer Tokens to Account “Tester”
Now that account user
has tokens, we will transfer some to account tester
. We indicate that user
authorized this action using the permission argument -p user
.
$ cleos push action eosio.token transfer '[ "user", "tester", "25.0000 EOS", "m" ]' -p user
executed transaction: bd6da0a70e386a5302d02c481d80a7bd19409169d0bab035090b1d3867d29391 256 bytes 111616 cycles
# eosio.token <= eosio.token::transfer {"from":"user","to":"tester","quantity":"25.0000 EOS","memo":"m"}
>> transfer
# user <= eosio.token::transfer {"from":"user","to":"tester","quantity":"25.0000 EOS","memo":"m"}
# tester <= eosio.token::transfer {"from":"user","to":"tester","quantity":"25.0000 EOS","memo":"m"}
7. Deploy Exchange Contract
Similar to the examples shown above, we can deploy the exchange
contract. The exchange
contract provides capabilities to create and trade currency. It is assumed this is being run from the root of the EOSIO source.
$ cleos create account eosio exchange EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
executed transaction: 6337ac311a5df1fb9ed9b1f8728b2ea67b2bc9663c06cf6e0797d12d7280572e 352 bytes 102400 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"exchange","owner":{"threshold":1,"keys":[{"key":"EOS5zgUiXJxDNvP8HJRrzXdS...
7.1. Deploy Eosio.msig Contract
The eosio.msig
contract allows multiple parties to sign a single transaction asynchronously. EOSIO has multi-signature (multisig) support at a base level, but it requires a synchronous side channel where data is ferried around and signed. Eosio.msig
is a more user friendly way of asynchronously proposing, approving and eventually publishing a transaction with multiple parties' consent.
The following steps can be used to deploy the eosio.msig
contract.
$ cleos create account eosio eosio.msig EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
executed transaction: ccf74668c29d9c581aa64692f9d24df43d0cdba40366bd1c33a598b2feb198d4 352 bytes 102400 cycles
# eosio <= eosio::newaccount {"creator":"eosio","name":"eosio.msig","owner":{"threshold":1,"keys":[{"key":"EOS5zgUiXJxDNvP8HJRrzX...
8. Deploy Hello World Contract
We will now create our first “hello world” contract. Create a new folder called “hello”, cd into the folder, then create a file “hello.cpp” with the following contents:
hello/hello.cpp
#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
using namespace eosio;
class hello : public eosio::contract {
public:
using contract::contract;
/// @abi action
void hi( account_name user ) {
print( "Hello, ", name{user} );
}
};
EOSIO_ABI( hello, (hi) )
You can compile your code to web assembly (.wast) as follows:
$ eosiocpp -o hello.wast hello.cpp
Now generate the abi:
$ eosiocpp -g hello.abi hello.cpp
Generated hello.abi
Create an account and upload the contract:
$ cleos create account eosio hello.code EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ EOS5zgUiXJxDNvP8HJRrzXdSEdnL3S4MW8c86257HAMyqmmz2dGsZ
...
$ cleos set contract hello.code ../hello -p hello.code
...
Now we can run the contract:
$ cleos push action hello.code hi '["user"]' -p user
executed transaction: 2b4691674e206f69fd68ea458ea152405d436ba19ae06af5a697d38bfbbca2e4 232 bytes 102400 cycles
# hello.code <= hello.code::hi {"user":"user"}
>> Hello, user
At this time the contract allows anyone to authorize it, we could also say:
$ cleos push action hello.code hi '["tester"]' -p tester
executed transaction: c9b765e80f3b31b8946f18f59ed3f70495af769acb4923799e3bf647ee1df8d5 232 bytes 102400 cycles
# hello.code <= hello.code::hi {"user":"tester"}
>> Hello, tester