This tutorial is not runnable. Its purpose is only to show you some of the fundamental eoslime functionality
Let's up the level of difficulty.
Scenario: Weare a simple Pizza shop. We have a contract and an account serving our shop.
Shop account has the following roles and responsibilities separated
Cooker - only he should be able to notify when a new Pizza has been cooked
Shop Manager - he could order more cooking products and withdraw the daily turnover. For the manager to withdraw the money, however, he should have the approval of the owners.
2 Owners - they could withdraw the daily turnover
The contract should have the following functionality
Ready Pizza. It should send the cooked Pizza to its buyer.
Order cooking products
Order a Pizza. By ordering we are notifying our cooker to start making it
Buy one piece of the whole Pizza per transaction
Withdraw the daily turnover. At the end of the day, the shop manager should be able to withdraw the turnover
Our scenario has been set. Let's have fun and write some code
Define our tests border
const assert = require('assert');
const SHOP_WASM_PATH = './contracts/shopcontract.wasm';
const SHOP_ABI_PATH = './contracts/shopcontract.abi';
describe('Shop contract', function (eoslime) {
// Increase mocha(testing framework) time, otherwise tests fails
this.timeout(15000);
let shopContract;
let shopAccount;
let cooker;
before(async () => {
shopAccount = await eoslime.Account.createRandom();
shopContract = await eoslime.Contract.deploy(SHOP_WASM_PATH, SHOP_ABI_PATH);
});
});
Every owner will have a private/public keys pair. These pairs should be registered in the owner's authority of the account. In this way, only the owners will be able to manage the account.
We should load the shop account with the owner's authority so we will be able to modify the authorities in it. We need to load it because when creating a random account, it comes with generated private/public keys pair. This is some kind of a 'default' keys
At the moment this authority is a brand new one and does not have access to anything in the blockchain. However, we need this person to be able to withdraw the daily turnover and order more cooking products.
We will start by giving our shop manager access to order cooking products
Nice job! We have created a custom authority and give some access to it. Let's continue with turnover access. To achieve this we will create another custom authority called "turnover". We need to configure it as a Multisignature one, so when the shop manager needs to withdraw the money, he should get at least one approval from the owners.
We have created the "turnover" authority because of the approvals. The "shopmanager" one does not need approvals to order more cooking products.
At the moment we have a brand new empty authority. Before adding its operators, let's set its approvals requisition.
await turnoverAuthority.increaseThreshold(2);
Each account's authority has a threshold value and each of the keys inside the authority have weight. We will set these values such as
Authority threshold = 2
Owners keys weight = 2
Shop manager keys weight = 1
The above configuration works as follow. Each of the owners is able to withdraw the turnover independently as their weight is at least equal to the threshold. The shop manager, however, needs his withdrawal transaction to be signed(approved) from at least one of the owners, because his weight is not enough.
We prepared our shop manager. Yeeey! It is time to set up our cooker. For that reason, let's generate for him new keys and create an authority called "cooker".
Fantastic! We have prepared our shop account and our little shop is ready to open its doors. It is Pizza time! I almost forget to tell you about the Pizza price...
A piece will cost 2 EOS tokens. Buy-Pizza method will accept only one argument - the buyer name, so the contract could verify our buyers are the actual signers of their transactions. A bit of security stuff...
// Let's create our customer Bob
const bob = await eoslime.Account.createRandom();
// Buy some delisious Pizza piece
await shopContract.actions.buy(bob.name, { from: bob, tokens: "2.0000 EOS" });
The object in the end contains options which are related to how the transaction will be structured and broadcasted
from - represents the signer of the transaction - Bob. To the moment our shopContract has its account as a signer of the transaction. If we leave it without from option, the shopContract will sign the transaction, but we need Bob to do it
tokens - In this way, you can process a transfer tokens in an atomic way. So you will process the buy action but you won't actually transfer tokens. You should do it in a separate transaction, but if it fails? You will register that you have bought a Pizza, but because of the transfer has failed, you did not give any tokens. tokens option package both of your transactions in an atomic one and if the transfer fails for some reason, you won't be able to buy a piece of Pizza
"Yummy Pizza!". Yeeah, Bob likes our Pizza and he wants to buy 2 more pieces.
Ouch... we forgot to notify our cooker.... Let's fix this for the feature
shopContract.actions.orderpizza.on('processed', (txReceipt, inputParams) => {
// Start cooking
await CookingService.startCooking();
// Once the Pizza becomes cooked, the cooker will notify the contract
// so it could deliver the pizza to its buyer
// inputParams[0] is bob's account name
await shopContract.actions.readypizza(cooker.name, inputParams[0], { from: cooker });
});
// Now, when someone orders a Pizza, our cooker will know he has a task
await shopContract.actions.orderpizza(bob.name, { from: bob, tokens: "10.0000 EOS" });
// Check a Pizza has been ordered
// Retrieve all pizzas bought from Bob in 'orders' table
const pizzas = await shopContract.tables.orders.equal(bob.name).find()
assert(pizzas[0].buyers == bob.name);
Our cheese seems to end very soon. Let's order more cooking products
await shopContract.actions.orderproduct('cheese', { from: shopManagerAuthority });
// Check a product order has been made
// Retrieve all products orders from 'coockingproducts' table
const orderedProducts = await shopContract.tables.coockingproducts.find();
assert(orderedProducts[0].product == 'cheese');
Our tutorial is coming to its end and our long first day in the shop also. It is time for the shop manager to withdraw the turnover.
Let's load the multisignature "turnover" authority
Once loaded our shop manager will be able to request a withdrawal approval
// Withdraw does not need any parameters to be provided
// that is why the array at the end is empty
const proposalId = await turnoverMultiSigAuthority.propose(shopContract.withdraw.actions, []);
// The first owner approves the withdrawal
await turnoverMultiSigAuthority.approve(firstOwnerKeys.publicKey, proposalId);
// The shop manager is ready now to process the withdrawal
await turnoverMultiSigAuthority.processProposal(proposalId);
We had a successful first day in our little shop. Nice work!
Here is how the complete tests could like
const assert = require('assert');
const SHOP_WASM_PATH = './contracts/shopcontract.wasm';
const SHOP_ABI_PATH = './contracts/shopcontract.abi';
describe('Shop contract', function (eoslime) {
// Increase mocha(testing framework) time, otherwise tests fails
this.timeout(15000);
let shopContract;
let shopAccount;
let cooker;
before(async () => {
shopAccount = await eoslime.Account.createRandom();
shopContract = await eoslime.Contract.deploy(SHOP_WASM_PATH, SHOP_ABI_PATH);
});
it('Should have two owners', async () => {
shopAccount = eoslime.Account.load(shopAccount.name, shopAccount.executor.privateKey, 'owner');
const firstOwnerKeys = await eoslime.utils.generateKeys();
const secondOwnerKeys = await eoslime.utils.generateKeys();
await shopAccount.addOnBehalfKey(firstOwnerKeys.publicKey)
await shopAccount.addOnBehalfKey(secondOwnerKeys.publicKey);
authorityInfo = await shopAccount.getAuthorityInfo();
// Check the owner authority has both owner keys
assert(authorityInfo.required_auth.keys.find((keyData) => { return keyData.key == keysPair.publicKey }));
});
it('Should have a shop manager with the ability to withdraw the daily turnover', async () => {
const shopManagerKeys = await eoslime.utils.generateKeys();
await shopAccount.addAuthority('shopmanager');
await shopAccount.setAuthorityAbilities('shopmanager', [
{ action: 'orderproducts', contract: shopContract.name }
]);
const shopManagerAuthority = eoslime.Account.load(shopAccount.name, shopAccount.privateKey, 'shopmanager');
// Add shop manager keys
await shopManagerAuthority.addOnBehalfKey(shopManagerKeys.publicKey);
});
it('Should have a turnover authority with the ability to withdraw the daily turnover', async () => {
await shopAccount.addAuthority('turnover');
// Add withdraw access
await shopAccount.setAuthorityAbilities('turnover', [
{ action: 'withdraw', contract: shopContract.name }
]);
const turnoverAuthority = eoslime.Account.load(shopAccount.name, shopAccount.privateKey, 'turnover');
await turnoverAuthority.increaseThreshold(2);
const ownerWeight = 2;
const shopManagerWeight = 1;
await turnoverAuthority.addOnBehalfKey(firstOwnerKeys.publicKey, ownerWeight);
await turnoverAuthority.addOnBehalfKey(secondOwnerKeys.publicKey, ownerWeight);
await turnoverAuthority.addOnBehalfKey(shopManagerAuthority.publicKey, shopManagerWeight);
});
it('Should have a cooker', async () => {
await shopAccount.addAuthority('cooker');
// Add readypizza access
await shopAccount.setAuthorityAbilities('cooker', [
{ action: 'readypizza', contract: shopContract.name }
]);
});
it('Should be able for a customer to buy a piece of our Pizza', async () => {
const bob = await eoslime.Account.createRandom();
// Buy two pieces of the pizza
await shopContract.actions.buy(bob.name, { from: bob, tokens: "2.0000 EOS" });
await shopContract.actions.buy(bob.name, { from: bob, tokens: "2.0000 EOS", unique: true });
});
it('Should be able for a customer to order a whole Pizza and receive it once it is ready', async () => {
shopContract.actions.orderpizza.on('processed', (txReceipt, inputParams) => {
await shopContract.actions.readypizza(cooker.name, inputParams[0], { from: cooker });
});
await shopContract.actions.orderpizza(bob.name, { from: bob, tokens: "10.0000 EOS" });
const orderedPizzas = await shopContract.tables.orderedpizzas.find();
assert(orderedPizzas[0].buyer == bob.name);
});
it('Should be able for shopper manager to order more cooking products', async () => {
await shopContract.actions.orderproduct('cheese', { from: shopManagerAuthority });
const orderedProducts = await shopContract.tables.coockingproducts.find();
assert(orderedProducts[0].product == 'cheese');
});
it('Should be able for shopper manager to withdraw the daily turnover', async () => {
const turnoverMultiSigAuthority = eoslime.MultiSigAccount.load(shopAccount.name, shopManagerAuthority.privateKey, 'turnover');
turnoverMultiSigAuthority.loadKeys([firstOwnerKeys.privateKey, secondOwnerKeys.privateKey]);
// Make a withdrawal proposal
const proposalId = await turnoverMultiSigAuthority.propose(shopContract.actions.withdraw, []);
// The first owner approves the withdrawal
await turnoverMultiSigAuthority.approve(firstOwnerKeys.publicKey, proposalId);
// Shop manager is able to process the withdrawal
await turnoverMultiSigAuthority.processProposal(proposalId);
// Check our shop account got the turnover
const shopBalance = await shopAccount.getBalance();
assert(shopBalance[0] == '14.0000 EOS', 'Incorrect tokens amount after send');
});
});