How to Sell Coupons on ETH? Build, Deploy and Test the Smart Contract (2/4)

How to Sell Coupons on ETH? Creating your own sample dApp in four parts:


Welcome to the 2nd part of the series.

In this part, we will see the implementation of Solidity code as we build the smart contract gradually.

πŸ›‘ Note: This is just a training “fun” dApp that shows you how to use React, Truffle, and Solidity. It is not a real dApp with real functionality!

Imaginary Scenario: Finxter academy has recently announced that it will launch some coupons with a 25% discount for its new premium membership, and the coupons will be distributed through a decentralized blockchain.

We will create a smart contract with 30 coupons for the new Finxter academy premium membership joiner, and each member can buy this coupon for a certain amount of ether.

When a coupon is sold, the account address of the member will be saved under the coupon, so there is no chance of reselling the same coupon twice. That means once they buy the coupon, the coupon is sold forever. There will be no chance to tamper with the transaction records.

Building the Smart Contract

So, move to the VS Code and create a solidity file named “Coupons.sol“. Make sure the Solidity extension is installed in your VS Code for a better coding experience.

Let’s define the solidity version first:

pragma solidity >=0.4.22 <0.9.0;

we will start writing our contract but before that, let’s declare a constant for the number of coupons we sell.

uint256 constant totalCoupons = 30;

Create the smart contract and declare the owner

contract Coupons {
  address public owner = msg.sender;
}

We have introduced a “Coupons” contract here, and any property written inside this contract will be saved as data into the Blockchain.

The first important thing in a smart contract is “who created that smart contract?”.

The owner variable declares the owner of the smart contract, and the “msg.sender” is the address that has initiated a function or called for a transaction.

In this case, “msg.sender” is the wallet address for the first account of the ganache.

struct Coupon {
    uint256 id;
    uint256 price;
    address owner;
  }

Structs allow us to group data in solidity.

We have created a struct for a coupon here. These coupons will have a unique ID and a static price property.

There will also be an address for the coupons that will help us determine who is the owner of that coupon.

Primarily we will set a static owner address, but when someone buys the coupon, his account address will be denoted as the owner of the coupon, and no one else will be able to repurchase it. It will be apparent to you when we will declare the state variables.

Coupon[totalCoupons] public coupons;

We have created a publicly accessible array of coupons here. The total number of coupons inside the array would be 30.

  constructor() {
    for (uint256 i = 0; i < totalCoupons; i++) {      
      coupons[i].price = 1e17; // 0.1 ETH
      coupons[i].owner = address(0x0);
      coupons[i].id = i ;
    }
  }

Constructors are mainly used to initialize state variables. We have initiated a for loop to loop through all the coupons.

The price for each coupon has been set to 0.1 eth. 1 Ethereum equals 1e18 wei. Wei is the smallest unit of ether. Here we are pricing 0.1 eth per coupon.

🌍 Recommended Tutorial: Introduction to Gas in Ethereum

We have defined the owner address as 0x0, i.e., a kind of not defined. When someone buys the coupon, this 0x0 will be replaced with the buyer’s account address.

Whenever we loop through the coupons, each coupon will take the index number as its id. In this way, it will be easier to detect which coupon is sold and which is still available for buying.

Be careful when creating these storage variables cause if you use this haphazardly, you must endure a high cost. So, you need to minimize the creation of storage variables and variable changes for cost-effectiveness.

function buyCoupon(uint256 _index) external payable {
    require(_index < totalCoupons && _index >= 0);
    require(coupons[_index].owner == address(0x0));
    require(msg.value >= coupons[_index].price);
    coupons[_index].owner = msg.sender;
  }

We have created a function for buying coupons that will take the index of the coupons as an argument. Since we have 30 coupons, the index number will range from 0 to 29.

This function should only be called from outside the contract; that’s why we are using an ‘external‘ quantifier.

The ‘payable‘ modifier allows any solidity function to send and receive ether. Since we will send transactions by calling this method, we have to use the ‘payable’ modifier here.

🌍 Reference

We need to refrain the users from providing invalid input. That’s why we use the ‘require‘ function to apply some conditions.

The index number must be less than 29 and more than 0.

Each coupon’s owner address will remain as (0x0) until someone buys it.

β€˜msg.value’ ensures that each buyer must send enough money to buy the coupon, i.e., 1e17 eth.

When the coupon is bought, we need to change the owner’s address, and we do this by setting the owner as “msg.sender“.

So, our Solidity contract is ready. As it is a static contract, you can not add or deduct any number of coupons afterward. So be sure if you want to bring any change.

Deploying the Smart Contract

Now it’s time to deploy our contract.

Create a new file inside the migration folder. We named it “2_coupons_migration.js“. for the migration, you can follow the structure of the ‘1_intial_migration.js’ file.

const Coupons = artifacts.require("Coupons");
module.exports = function (deployer) {
  deployer.deploy(Coupons);
};

This artifacts method saves the application binary interface (ABI) and other info related to the contract.

If you need to deploy the contract again, you do not have to recompile the contract. It will take the contract bytecode from the JSON file that will be created after the deployment.

In short, a truffle artifact is the JSON file of the contract that contains a lot of useful information related to a contract, like the ABI, the contract bytecode, the deployment details, the compiler version, etc.

This function tells the truffle that I want to deploy the contract. let’s run the command on the terminal

truffle migrate

truffle knows that it ran the migration for the 1st file. It will automatically start the migration for the β€˜2_coupons_migration.js’ file.

Now If you notice the first account of ganache, you will see some ether has been deducted to run the transaction.

Move to the CONTRACTS menu. You will see the “Coupons” contract has already been deployed. Inside the storage section, you will find an array of 30 slots and a bunch of coupons owned by no one.

Testing the Smart Contract

So we have successfully deployed our contract, but before building out our finxter dApp, we need to run tests.

We are going to put things up on the blockchain, and once it is loaded on the blockchain, we can’t change their state. The more efficient tests we run, the better it will be in the long term.

Let’s create a directory first. Inside the test folder, create a file named ‘Coupons_test.js’ where we will write our test.

const Coupons = artifacts.require('Coupons');
const assert = require('assert');

We are importing the ‘Coupons‘ contract with the first line of code. “artifacts.require()” will take the class name we want to work with.

We need to use the assert function here. The assert function is used to throw an error if the expression passed into the function is false.

contract("Coupons", (accounts) => {
  const buyerAccount = accounts[1];
  const couponID = 0;

  it("should allow a user to buy a coupon", async () => {
    const instance = await Coupons.deployed();
    const availableCoupon = await instance.coupons(couponID);
    await instance.buyCoupon(couponID, {
      from: buyerAccount,
      value: availableCoupon.price,
    });
    const updatedCoupon = await instance.coupons(couponID);
    assert.equal(
      updatedCoupon.owner,
      buyerAccount,
      "The buyer owns the coupon"
    );
  });
});

We have created a contract for writing our tests. This contract takes two parameters.

  • The first is the contract name β€œCoupons” on which we will run our test.
  • The second is a callback function that takes the ganache accounts as the parameter.

We need to keep track of who bought the coupon, and that’s why we defined the buyer account with the “buyerAccount” constant.

The it() function is used in test runners like jasmine or karma, which is only used when you are testing. The purpose of it() is to automate a bunch of regular tasks a test runner usually does and validate if all the tasks’ responses or events are working correctly.

We created an instance of our contract with the “instance” constant.

Firstly, we need to ensure that someone does not previously buy the coupon we want. With the β€˜availabeCoupon’ constant, we call the coupons array and pass the coupon number as a parameter to buy our choice.

With the help of instance, we called the publicly accessible coupons array we had created on the smart contract.

So basically, we are instructing the function when the contract is deployed; you are going to grab the first coupon that is index 0. We set the couponID as index 0.

Now it’s time to buy a coupon.

We called the buyCoupon function from the smart contract with β€˜instance.buyCoupon’.

We passed the couponID of the coupon we wanted to buy as the first parameter. As this is a payable method, we need to confirm the buyer account and the amount required to purchase the coupon. 'from' and 'value' keys, respectively, took the buyer account and the price of the coupon as values.

In simple words, the buyer is saying I want to buy a coupon with a specific ID, and I am providing my account address and the price set for that coupon.

Now the coupon has been bought. So, we need to update the owner address of that coupon with the buyer’s account address. The β€˜assert.equal’ method checks if the owner of the coupon ID is the buyer. If all are okay, it will print, "The buyer owns this coupon."

Let’s run the test in the terminal with the command

truffle test test/ Coupons_test.js

Don’t forget to locate the directory of the test file. The process will be done in an isolated Ethereum network.

It may cost some money from the 2nd account of ganache for the test purpose. You will get the test result in your terminal if everything acts appropriately.

This wraps up the 2nd part of the video. I tried to explain how to write a smart contract and run tests on that. You can change the value and run multiple tests if you want. I hope you enjoyed it.

You can get the code from GitHub.

Here are all links to this 4-part code project to create a sample dApp on ETH for selling virtualized coupon codes: