In the previous article we created a Tezos Smart Contract in Fi that allows for only authenticated voters to vote on a ballot. This contract opens the door for delegation services to account for their delegations opinions in a decentralized anonymous manner. It also opens the door for general voting.

See the contract below:

struct Proposal (

string description,

int votes

); storage map[address=>bool] voter;

storage map[int=>Proposal] ballot;

storage address manager;

storage string info; entry addAuthorizedVoter(address addr){

if (SENDER == storage.manager) {

storage.voter.push(input.addr, bool false);

} else {

throw(string "Not authorized to add a voter.");

}

} entry addProposal(int id, Proposal proposal){

if (SENDER == storage.manager) {

storage.ballot.push(input.id, input.proposal);

} else {

throw(string "Not authorized to add a ballet.");

}

} entry addInfo(string desc){

if (SENDER == storage.manager) {

storage.info = input.desc;

} else {

throw(string "Not authorized to set description.");

}

} entry placeVote(int vote){ if (in(storage.voter, SENDER) == bool false) {

throw(string "You are not authorized to vote.");

}



if (in(storage.ballot, input.vote) == bool false) {

throw(string "No proposal exists for your vote.");

}





let bool myVote = storage.voter.get(SENDER);

let Proposal proposal = storage.ballot.get(input.vote);



if (myVote == bool false) {

myVote = bool true;



proposal.votes.add(int 1);

storage.ballot.push(input.vote, proposal);

storage.voter.push(SENDER, myVote);

} else {

throw(string "You have already voted. Voters may only vote once.");

}

}

In Part II of this tutorial we are going to show how you can use TezTech’s tools to pragmatically manage the above contract by using the power of the Fi-Compiler NodeJS library along the eztz library to automate the process.

Compiling The Contract

Let’s start by deploying the contract on Tezos Alphanet. First we need to compile the above code into Michelson. We can do this in two ways, one by using the online Fi-Comipiler tool, or by downloading the Fi-Compiler and installing it from source. We’ll walk through both.

Using the Online Fi-Compiler

Navigate to https://fi-code.com/. You should see a text editor on your left, and the Michelson text box on your right. Paste the above voting contract in the Fi-Editor on you’re right. Then hit the compile button.

Using the Online Fi-Compiler

After hitting the compile button you should see the Fi code compiled into Michelson on you’re right. Now simply copy the Michelson by hitting the copy button, and save it into a file called delegationVoting.fi.ml.

Using the Fi-Compiler From Source

Although the online Fi Compiler is perfectly suitable and trustworthy, it is always good practice to use coding tools from their source to ensure authenticity, especially in this case where we are dealing with contracts of value. If you’ve already been following my tutorials, a lot of what’s below will be redundant with compilation instructions from Getting Started With Fi.

Let’s start by installing the necessary prerequisites, by installing NodeJS and NPM. If you are using a debian based distribution you can use the following tutorial to do so: https://tecadmin.net/install-latest-nodejs-npm-on-debian/NPM

Now let’s clone the source code of the Fi-Compiler from TezTech’s public repository.

Next enter the directory from your terminal, and run the install command:

cd fi-compiler

npm i -g fi-cli

If you receive a permissions error as a result of the above, try running the same command with sudo .

Once you’ve successfully installed the fi-compiler take the above voting contract and save into a file called delegationVoting.fi onto your machine. Now let’s use the compiler to compile the file delegationVoting.fi.

fi compile delegationVoting.fi

And as a result you will now see the compiled Michelson in a file called delegationVoting.fi.ml.

Deploying The Contract

Originating The Contract

Now that we have compiled our Fi code down to Michelson we can use delegationVoting.fi.ml to originate a contract. First let’s take a look into the Michelson file to see the storage primitives our Fi contract is expecting.

(pair (map address bool) (pair (map int (pair string int)) (pair address string)))

Looking at the Michelson version of the contract we can see that we are expecting a pair of our voters map with the ballot map paired with another pair of the manager address and info string. This probably looks odd to you, and the reason Michelson pairs the storage in this way if for it’s formal verification potential.

Now that we know what data primitives are expected, we can figure out how to structure the initialization of the storage.

Pair {Elt "tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG" False} (Pair {Elt 1 (Pair "Gas limit increase and roll reduction to 8000" 0)} (Pair "tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG" "Voting on Athens A and B"))

You can see the initialization of the voter storage with the line below.

{Elt "tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG" False}

Likewise the initialization of one of the proposals with ID 0.

{Elt 1 (Pair "Gas limit increase and roll reduction to 8000" 0)

Then we initialize the manager of the contract and the info string.

"tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG"

"Voting on Athens A and B"

All these values are coupled together by the appropriate pairs as described in the Michelson version of the contract. Now that we know what storage data we are going to initialize the contract with, let’s originate the contract.

./tezos-client originate contract voting for wallet transferring 0 from wallet running ../fi-contracts/delegationVoting.fi.ml --init 'Pair {Elt "tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG" False} (Pair {Elt 1 (Pair "Gas limit increase and roll reduction to 8000" 0)} (Pair "tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG" "Voting on Athens A and B"))' --burn-cap 3.244

Automating The Contract

Why do we need automation in this contract? This contract requires quite a bit of input that would be incredibly tedious to try to manage manually. For example say a large delegation service needed to add all their delegations as authorized voters to vote. You’re potentially talking about invoking the addAuthorizedVoter up to thousands of times. To gather this data you would have to comb through each delegation one by one and run the necessary arguments with the tezos-client. Although not impossible, that seems like a very challenging task.

Let’s take this one step further though, since we are already automating the managing steps of this contract, why not create a program that streamlines the voting process for the authorized voters as well. This way the voters themselves don’t have to be technically skilled to vote, and the barrier to entry is easier.

Hold tight, because this may be a long road ahead. If your more interested in using the tool itself verses how the tool is made feel free to skip to Using The Program section of this tutorial.

Installation

First we will need to make our program is install the correct dependencies. Create a folder called DelegeationVoting and inside a file called delegationVoting.js. Then inside the folder run:

npm install fi-compiler

npm install js-yaml

npm install minimist

The first package installed is the fi-compiler package. The next package is a yaml library used for parsing a configuration file we will use later, and the third package is used to manage command line input.

Next clone the eztz repository to a place of your choosing (In my case my home directory).

Then inside the eztz folder run the following commands:

npm install webpack

npm run-script build

Now open the delegationVoting.js file and add the following as imports.

var fi = require("fi-compiler");

var eztz = require("~/eztz/").eztz

var stdio = require('stdio');

var yaml = require('js-yaml');

var fs = require('fs');

var argv = require('minimist')(process.argv.slice(2));

Building

Now that we have our dependencies installed and the imports needed to write our program let’s begin writing by placing our contract in our code.

var ficode = `

struct Proposal (

string description,

int votes

); storage map[address=>bool] voter;

storage map[int=>Proposal] ballot;

storage address manager;

storage string info; entry addAuthorizedVoter(address addr){

if (SENDER == storage.manager) {

storage.voter.push(input.addr, bool false);

} else {

throw(string "Not authorized to add a voter.");

}

} entry addProposal(int id, Proposal proposal){

if (SENDER == storage.manager) {

storage.ballot.push(input.id, input.proposal);

} else {

throw(string "Not authorized to add a ballet.");

}

} entry addInfo(string desc){

if (SENDER == storage.manager) {

storage.info = input.desc;

} else {

throw(string "Not authorized to set description.");

}

} entry placeVote(int vote){ if (in(storage.voter, SENDER) == bool false) {

throw(string "You are not authorized to vote.");

}



if (in(storage.ballot, input.vote) == bool false) {

throw(string "No proposal exists for your vote.");

}





let bool myVote = storage.voter.get(SENDER);

let Proposal proposal = storage.ballot.get(input.vote);



if (myVote == bool false) {

myVote = bool true;



proposal.votes.add(int 1);

storage.ballot.push(input.vote, proposal);

storage.voter.push(SENDER, myVote);

} else {

throw(string "You have already voted. Voters may only vote once.");

}

}

`;

As you can see we store our contract in a variable called ficode. Next we are going to use ficode and compile it using the fi.compile method from the fi compiler.

var compiled = fi.compile(ficode);

fi.abi.load(compiled.abi);

Then we load the compiled abi into the fi-compiler library. The abi is a helper to allow you to form byte arguments that Michelson can understand.

Previously we installed the minimist library which we are going to use below to take in command line arguments.

if (argv.alphanet) {

eztz.node.setProvider("https://alphanet-node.tzscan.io/")

} else if (argv.network) {

eztz.node.setProvider(argv.network)

} else {

eztz.node.setProvider("https://mainnet-node.tzscan.io/")

}

Let’s first take a command line argument telling our program what network to use. We will accept the --alphanet flag to signify to use tzscan’s public alphanet node. We will use the --network=<rpc_api> flag to signify a custom network. And finally if no flag is passed we default to tzscan’s public mainnet node.

if (argv.addvoters) {

addAuthorizedVoters(argv.addvoters, wallet)

}

We then should add a flag to signify we want to add voters to our contract, denoted --addvoters=<delegate_pkh> . If the addvoters flag is passed we invoke the addAuthorizedVoters function (defined later), which takes in the delegate_pkh passed with the flag, and the keys to the wallet invoking it. Remember our smart contract will only allow the originator to addvoters.

if (argv.addvoter) {

input = {

addr: argv.addvoter

}

addAuthorizedVoter(input, wallet)

}

Similarly, we create an option to just add a single voter denoted --addvoter=<pkh> . If this flag is passed, we construct an input object that the abi can understand in perspective to the entry addAuthorizedVoter(address addr) entry in our ficode. We then invoke the addAuthorizedVoter method (defined later) which takes the input, and wallet keys as a parameter.

if (argv.addinfo) {

input = {

desc: argv.addinfo

}

addInfo(input, wallet)

}

Next we created an option to invoke the addInfo(string desc) entry in our ficode, denoted as --addinfo="info string" . We construct the input expected by the contract and pass it to the addInfo(input, wallet) function (defined later), with the input and wallet keys as a parameter.

if (argv.addproposal) {

if (argv.proposalid) {

input = {

id: argv.proposalid,

proposal: argv.addproposal

}

addProposal(input, wallet)

} else {

console.log("Must also specify a proposalid (int).")

}

}

Next we accept the ability to invoke the addProposal(int id, Proposal proposal) entry in our ficode. We use the --addproposal="proposal string" and the --proposalid=<int> flag to construct our proposal input. If only the --addproposal flag is supplied, the proposal will not be added because it is missing the proposal id. If both flags are passed, we invoke the addProposal(input, wallet) function (defined later), which takes the input contructed and wallet keys.

if (argv.placevote) {

input = {

vote: argv.placevote

}

placeVote(input, wallet)

}

The next option we allow, for the voters, is the ability to place a vote, denoted as --placevote=<int> . If the --placevote flag is invoked we contruct the input and pass it to the placeVote(input, wallet) function (defined later), along with the keys to the wallet invoking.

var gas_limit;

var fee;

if (argv.gaslimit) {

gas_limit = argv.gaslimit;

}

if (argv.fee) {

fee = argv.fee;

}

Finally the last two options we’ll pass, will allow the user to specify the gas_limit of the operation as well as the fee. These will be required fields, so put these at the top of all the options we previously defined. Each operation call to our smart contract will include these variables.

Now that we have defined the options our program can handle let’s address the wallet that consistently came up before. In order to contsruct an operation the Tezos network needs your wallet to sign the operation so that it is authentic. In order to handle this requirement we need to a way to pass in the wallet information.

To do this we will use the yaml syntax to write a configuration file. Create a file called conf.yaml and paste the contents below.

wallet:

sk: "edsk327MQMV16rhKtT1FZPKoUsX3uTrnSgWxayq6h5iAdyXE7rxEQa"

contract: "KT1CRbGv5Mv2bw5PuqQu5qAeJgeMEANuuwhx"

First we define a wallet object in yaml, with a field sk for secret key. Paste the secret key of your wallet into the conf file. Note: This is for alphanet demonstration. The secret key above is an unencrypted alphanet wallet. For mainnet use, you can follow this tutorial and paste an encrypted secret key, but manage with care.

The next thing we do is create a contract object that contains the address to the ficode contract deployed on alphanet, this will be used later.

Now let’s create a way to parse the conf.yaml file, and store it in an object we can traverse for later use.

var conf;

try {

const config = yaml.safeLoad(fs.readFileSync('conf.yaml','utf8'));

conf = JSON.stringify(config, null, 4);

} catch (e) {

console.log(e);

} conf = JSON.parse(conf);

Paste the above code above the ficode variable defined before. Basically all this code bit does is try to read in the yaml file using our yaml library installed before, and then parse it into a conf object we can use.

Now that we have a conf file let’s use the eztz library to extract the keys to our wallet based off the secret key provided. Paste the below code after the fi.abi.load() function from before.

var wallet = eztz.crypto.extractKeys(conf.wallet.sk)

Tada! And now we have access to our wallet which we can use to create operations to invoke our deployed contract. It is worth noting that if you are extracting encrypted keys you can use the function below.

eztz.crypto.extractEncryptedKeys(conf.wallet.sk, "Your password.")

After all this, we can finally get into the meat of our code, or the salad for you vegan folks. Before, we previously touched light of a few functions inside our options handling. We’re going to define these next, but you can see those functions below:

addAuthorizedVoter(input, keys)

addAuthorizedVoters(addr, keys)

addProposal(input, keys)

addInfo(input, keys)

placeVote(input, keys)

addAuthorizedVoter

We’ll start with addAuthorizedVoter(input, keys).

function addAuthorizedVoter(input, keys) {

vbytes = fi.abi.entry("addAuthorizedVoter", input)

vbytes = vbytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": vbytes

}

}; eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

}

Starting off, this function takes in input and keys . The input in this case is the the address you’d like to add, and the keys are the keys to the wallet your calling from. The next two lines around vbytes have to do with stripping the 0x off the string and passing the rest of vbytes into the operation parameter.

We then define the operation of type transaction, passing 0 XTZ, and invoking the contract address defined in conf.contract (remember the conf variable defined before). We also set the fee, and gas_limit to the variables defined before from our command line options.

Finally here is where the magic happens, we use the eztz library to invoke the rpc.sendOperation function. We send the operation by passing the public key hash of the wallet, the operation itself, and then the keys to your wallet (for signing). If successful, we print out the results, and likewise if unsuccessful the error.

addAuthorizedVoters

Next let’s take a look at addAuthorizedVoters, a function that will pull all delegations from a delegation service and add each delegation address as an authorized voter.

function addAuthorizedVoters(addr, keys) {

var query = "/chains/main/blocks/head/context/delegates/" + addr + "/delegated_contracts";

eztz.node.query(query).then(function (res) {

res.forEach(function (contract) {

var input = {

addr: contract

}; addAuthorizedVoter(input, keys)

});

}).catch(function (e) {

console.log(e)

});

}

In order to do this, we accept a parameter addr (address of the delegation service) and again keys . We then construct a query to get all the delegation addresses of the delegate at the parameter addr. Next we execute the query using the eztz library and then loop through each delegation address; denoted as contract . We then call the addAuthorizedVoter function defined previously for each contract in the loop.

addProposal

function addProposal(input, keys) {

pbytes = fi.abi.entry("addProposal", input)

pbytes = pbytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": pbytes

}

}; eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

}

The addProposal function, is nearly identitcal to the addAuthorizedVoter function. Except with the difference of forming the byters in the first line, where the abi is told to form them for the “addProposal” entry of our contract.

addInfo

function addInfo(input, keys) {

ibytes = fi.abi.entry("addInfo", input)

ibytes = ibytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": ibytes

}

}; eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

}

Again this should all look familiar. Except the abi is told to form the input bytes for the “addInfo” entry of our contract.

placeVote

function placeVote(input, keys) {

vbytes = fi.abi.entry("placeVote", input)

vbytes = vbytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": vbytes

}

};

eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

}

And again, note the abi is told to form the input bytes for the “placeVote” entry of our contract.

Putting the Program Together

You are now well on your way to being an excellent full-stack-smart-contract developer (what a mouthful!), but only with the help of TezTech’s software tools. That being said let’s finally piece our code together.

var fi = require("fi-compiler");

var eztz = require("/home/brice/eztz/").eztz

var yaml = require('js-yaml');

var fs = require('fs');

var stdio = require('stdio');

var argv = require('minimist')(process.argv.slice(2)); var conf; var conf;

try {

const config = yaml.safeLoad(fs.readFileSync('conf.yaml','utf8'));

conf = JSON.stringify(config, null, 4);

} catch (e) {

console.log(e);

}

conf = JSON.parse(conf); var ficode = `

struct Proposal (

string description,

int votes

); storage map[address=>bool] voter;

storage map[int=>Proposal] ballot;

storage address manager;

storage string info; entry addAuthorizedVoter(address addr){

if (SENDER == storage.manager) {

storage.voter.push(input.addr, bool false);

} else {

throw(string "Not authorized to add a voter.");

}

} entry addProposal(int id, Proposal proposal){

if (SENDER == storage.manager) {

storage.ballot.push(input.id, input.proposal);

} else {

throw(string "Not authorized to add a ballet.");

}

} entry addInfo(string desc){

if (SENDER == storage.manager) {

storage.info = input.desc;

} else {

throw(string "Not authorized to set description.");

}

} entry placeVote(int vote){ if (in(storage.voter, SENDER) == bool false) {

throw(string "You are not authorized to vote.");

}



if (in(storage.ballot, input.vote) == bool false) {

throw(string "No proposal exists for your vote.");

}





let bool myVote = storage.voter.get(SENDER);

let Proposal proposal = storage.ballot.get(input.vote);



if (myVote == bool false) {

myVote = bool true;



proposal.votes.add(int 1);

storage.ballot.push(input.vote, proposal);

storage.voter.push(SENDER, myVote);

} else {

throw(string "You have already voted. Voters may only vote once.");

}

}

`; var compiled = fi.compile(ficode);

fi.abi.load(compiled.abi); var wallet = eztz.crypto.extractKeys(conf.wallet.sk) var gas_limit;

var fee;

if (argv.gaslimit) {

gas_limit = argv.gaslimit;

}

if (argv.fee) {

fee = argv.fee;

} if (argv.alphanet) {

eztz.node.setProvider("https://alphanet-node.tzscan.io/")

} else if (argv.network) {

eztz.node.setProvider(argv.network)

} else {

eztz.node.setProvider("https://mainnet-node.tzscan.io/")

} if (argv.addvoters) {

addAuthorizedVoters(argv.addvoters, wallet)

} if (argv.addvoter) {

input = {

addr: argv.addvoter

}

addAuthorizedVoter(input, wallet)

} if (argv.addinfo) {

input = {

desc: argv.addinfo

}

addInfo(input, wallet)

} if (argv.addproposal) {

if (argv.proposalid) {

input = {

id: argv.proposalid,

proposal: argv.addproposal

}

addProposal(argv.addproposal, wallet)

} else {

console.log("Must also specify a proposalid (int).")

}

} if (argv.placevote) {

input = {

vote: argv.placevote

}

placeVote(input, wallet)

} function placeVote(input, keys) {

vbytes = fi.abi.entry("placeVote", input)

vbytes = vbytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": vbytes

}

};

eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

} function addInfo(input, keys) {

ibytes = fi.abi.entry("addInfo", input)

ibytes = ibytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": ibytes

}

}; eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

} function addProposal(input, keys) {

pbytes = fi.abi.entry("addProposal", input)

pbytes = pbytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": pbytes

}

}; eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

} function addAuthorizedVoter(input, keys) {

vbytes = fi.abi.entry("addAuthorizedVoter", input)

vbytes = vbytes.substr(2);

var operation = {

"kind": "transaction",

"amount": "0",

"destination": conf.contract,

"fee": fee,

"gas_limit": gas_limit,

"parameters": {

"bytes": vbytes

}

}; eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {

console.log(res);

}).catch(function (e) {

console.log(e)

});

} function addAuthorizedVoters(addr, keys) {

var query = "/chains/main/blocks/head/context/delegates/" + addr + "/delegated_contracts";

eztz.node.query(query).then(function (res) {

res.forEach(function (contract) {

var input = {

addr: contract

}; addAuthorizedVoter(input, keys)

});

}).catch(function (e) {

console.log(e)

});

}

Using The Program

After all that hard work, we can finally use our program to manage the voting contract without having to know the inner workings of Tezos itself, as well as allow our program to be utilized by our voters!

Let’s run through a couple of use cases of our programs use.

— addvoters

node delegationVoting.js --addvoters=<delegate_pkh> --gaslimit=<mutez> --fee=<mutez>

The above command uses our program to add all delegations of a delegate at <delegate_pkh> as authorized voters, with the specified gaslimit and fee to be used for each operation.

— addvoter

node delegationVoting.js --addvoter=<pkh> --gaslimit=<mutez> --fee=<mutez>

The above command uses our program very similar to --addvoters but instead takes an individual address to be added as an authorized voter.

— addinfo

node delegationVoting.js --addinfo="Info string" --gaslimit=<mutez> --fee=<mutez>

The above call to our manager program will change the info storage of our smart contract. Note this will only work, if the wallet defined in the yaml file is the address associated manager storage address in our contract.

— addproposal/ — proposalid

node delegationVoting.js --addproposal="Proposal string" --proposalid=<int> --gaslimit=<mutez> --fee=<mutez>

Similary to --addinfo , --addproposal will only work if the wallet defined in the conf file is associated with the manager storage address. Differently though, this only works by also adding --proposalid . By invoking these options you will then be able to add a proposal to the ballot storage of our contract, mapped by the id provided.

— placevote

node delegationVoting.js --placevote=<int> --proposalid=<int> --gaslimit=<mutez> --fee=<mutez>

Finally, and arguably the most important, the --placevote option. This allows any authorized voter to vote for a proposal in the ballot storage of our contract.

Conclusion

Congratulations if you’ve made it this far. Hopefully it wasn’t without feeling acomplished. Together with TezTech’s software libraries including the fi-compiler and eztz we can make our smart contracts more manageable and accessible to the public. In this tutorial we control our program through command line, but hopefully as a developer this will give you enough insight to build backend functions for amazing UI/UX’s you are set to design.

As a quick rehash we learned:

How to compile Fi Code inside NodeJs and use the ABI

How to write various functions to form operations to execute our smart contract.

How to utilize the eztz library to access the various RPC’s of Tezos.

Resources