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
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
Your final test suite for the above preparation could like such as
it('Should have two owners',async () => { shopAccount =eoslime.Account.load(shopAccount.name,shopAccount.executor.privateKey,'owner');constfirstOwnerKeys=awaiteoslime.utils.generateKeys();constsecondOwnerKeys=awaiteoslime.utils.generateKeys();awaitshopAccount.addOnBehalfKey(firstOwnerKeys.publicKey)awaitshopAccount.addOnBehalfKey(secondOwnerKeys.publicKey); authorityInfo =awaitshopAccount.getAuthorityInfo();// Check the owner authority has both owner keysassert(authorityInfo.required_auth.keys.find((keyData) => { returnkeyData.key ==keysPair.publicKey }));});
Once we have our owners added to the shop account, let's add our shop manager.
Generate a random private/public keys pair for him and add his keys in a new custom authority called "shopmanager".
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.
awaitturnoverAuthority.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...
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 cookingawaitCookingService.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 nameawaitshopContract.actions.readypizza(cooker.name, inputParams[0], { from: cooker });});// Now, when someone orders a Pizza, our cooker will know he has a taskawaitshopContract.actions.orderpizza(bob.name, { from: bob, tokens:"10.0000 EOS" });// Check a Pizza has been ordered// Retrieve all pizzas bought from Bob in 'orders' tableconstpizzas=awaitshopContract.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
awaitshopContract.actions.orderproduct('cheese', { from: shopManagerAuthority });// Check a product order has been made// Retrieve all products orders from 'coockingproducts' tableconstorderedProducts=awaitshopContract.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 emptyconstproposalId=awaitturnoverMultiSigAuthority.propose(shopContract.withdraw.actions, []);// The first owner approves the withdrawalawaitturnoverMultiSigAuthority.approve(firstOwnerKeys.publicKey, proposalId);// The shop manager is ready now to process the withdrawalawaitturnoverMultiSigAuthority.processProposal(proposalId);
We had a successful first day in our little shop. Nice work!
Here is how the complete tests could like
constassert=require('assert');constSHOP_WASM_PATH='./contracts/shopcontract.wasm';constSHOP_ABI_PATH='./contracts/shopcontract.abi';describe('Shop contract',function (eoslime) {// Increase mocha(testing framework) time, otherwise tests failsthis.timeout(15000);let shopContract;let shopAccount;let cooker;before(async () => { shopAccount =awaiteoslime.Account.createRandom(); shopContract =awaiteoslime.Contract.deploy(SHOP_WASM_PATH,SHOP_ABI_PATH); });it('Should have two owners',async () => { shopAccount =eoslime.Account.load(shopAccount.name,shopAccount.executor.privateKey,'owner');constfirstOwnerKeys=awaiteoslime.utils.generateKeys();constsecondOwnerKeys=awaiteoslime.utils.generateKeys();awaitshopAccount.addOnBehalfKey(firstOwnerKeys.publicKey)awaitshopAccount.addOnBehalfKey(secondOwnerKeys.publicKey); authorityInfo =awaitshopAccount.getAuthorityInfo();// Check the owner authority has both owner keysassert(authorityInfo.required_auth.keys.find((keyData) => { returnkeyData.key ==keysPair.publicKey })); });it('Should have a shop manager with the ability to withdraw the daily turnover',async () => {constshopManagerKeys=awaiteoslime.utils.generateKeys();awaitshopAccount.addAuthority('shopmanager');awaitshopAccount.setAuthorityAbilities('shopmanager', [ { action:'orderproducts', contract:shopContract.name } ]);constshopManagerAuthority=eoslime.Account.load(shopAccount.name,shopAccount.privateKey,'shopmanager');// Add shop manager keysawaitshopManagerAuthority.addOnBehalfKey(shopManagerKeys.publicKey); });it('Should have a turnover authority with the ability to withdraw the daily turnover',async () => {awaitshopAccount.addAuthority('turnover');// Add withdraw accessawaitshopAccount.setAuthorityAbilities('turnover', [ { action:'withdraw', contract:shopContract.name } ]);constturnoverAuthority=eoslime.Account.load(shopAccount.name,shopAccount.privateKey,'turnover');awaitturnoverAuthority.increaseThreshold(2);constownerWeight=2;constshopManagerWeight=1;awaitturnoverAuthority.addOnBehalfKey(firstOwnerKeys.publicKey, ownerWeight);awaitturnoverAuthority.addOnBehalfKey(secondOwnerKeys.publicKey, ownerWeight);awaitturnoverAuthority.addOnBehalfKey(shopManagerAuthority.publicKey, shopManagerWeight); });it('Should have a cooker',async () => {awaitshopAccount.addAuthority('cooker');// Add readypizza accessawaitshopAccount.setAuthorityAbilities('cooker', [ { action:'readypizza', contract:shopContract.name } ]); });it('Should be able for a customer to buy a piece of our Pizza',async () => {constbob=awaiteoslime.Account.createRandom();// Buy two pieces of the pizzaawaitshopContract.actions.buy(bob.name, { from: bob, tokens:"2.0000 EOS" });awaitshopContract.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) => {awaitshopContract.actions.readypizza(cooker.name, inputParams[0], { from: cooker }); });awaitshopContract.actions.orderpizza(bob.name, { from: bob, tokens:"10.0000 EOS" });constorderedPizzas=awaitshopContract.tables.orderedpizzas.find();assert(orderedPizzas[0].buyer ==bob.name); });it('Should be able for shopper manager to order more cooking products',async () => {awaitshopContract.actions.orderproduct('cheese', { from: shopManagerAuthority });constorderedProducts=awaitshopContract.tables.coockingproducts.find();assert(orderedProducts[0].product =='cheese'); });it('Should be able for shopper manager to withdraw the daily turnover',async () => {constturnoverMultiSigAuthority=eoslime.MultiSigAccount.load(shopAccount.name,shopManagerAuthority.privateKey,'turnover');turnoverMultiSigAuthority.loadKeys([firstOwnerKeys.privateKey,secondOwnerKeys.privateKey]);// Make a withdrawal proposalconstproposalId=awaitturnoverMultiSigAuthority.propose(shopContract.actions.withdraw, []);// The first owner approves the withdrawalawaitturnoverMultiSigAuthority.approve(firstOwnerKeys.publicKey, proposalId);// Shop manager is able to process the withdrawalawaitturnoverMultiSigAuthority.processProposal(proposalId);// Check our shop account got the turnoverconstshopBalance=awaitshopAccount.getBalance();assert(shopBalance[0] =='14.0000 EOS','Incorrect tokens amount after send'); });});