Implementing the Smart Contract



As documented in the previous post, I built out the application skeleton which came with an auto-generated sample Solidity smart contract.

A smart contract is a self-executing protocol with the terms of the agreement between two parties (e.g. buyer and seller) being directly written into lines of code. The code and the agreements contained therein exist across a distributed, decentralized blockchain network.

With that in place, the next item on the docket would be to create Final Static’s smart contract.

Coding the smart contract

In the /contracts/ folder, there’s already a sample smart contract code file with a .sol extension. To set up our own smart contract, I created a new file named FinalStatic.sol in the same folder.

Here’s the entirety of the file thus far:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "hardhat/console.sol";

contract FinalStatic is ERC721URIStorage
{
    using Counters for Counters.Counter;
    Counters.Counter private _token_ids;
    Counters.Counter private _items_sold;

    uint256 listing_price = 0.025 ether;
    address payable owner;

    mapping(uint256 => MarketItem) private id_to_market_item;

    struct MarketItem
    {
        uint256 token_id;
        address payable seller;
        address payable owner;
        uint256 price;
        bool is_sold;
    }

    event MarketItemCreated
    (
        uint256 indexed token_id,
        address seller,
        address owner,
        uint256 price,
        bool is_sold
    );

    constructor() ERC721("Metaverse Tokens", "METT")
    {
        owner = payable(msg.sender);
    }

    // Updates the listing price of the contract
    function updateListingPrice(uint _listing_price) public payable
    {
        require(owner == msg.sender, "Only marketplace owner can update listing price.");
        listing_price = _listing_price;
    }

    // Returns the listing price of the contract
    function getListingPrice() public view returns (uint256)
    {
        return listing_price;
    }

    // Mints a token and lists it in the marketplace
    function createToken(string memory token_URI, uint256 price) public payable returns (uint)
    {
        _token_ids.increment();
        uint256 new_token_id = _token_ids.current();

        _mint(msg.sender, new_token_id);
        _setTokenURI(new_token_id, token_URI);
        createMarketItem(new_token_id, price);
        return new_token_id;
    }

    function createMarketItem(uint256 token_id, uint256 price) private
    {
        require(price > 0, "Price must be at least 1 wei");
        require(msg.value == listing_price, "Price must be equal to listing price");

        id_to_market_item[token_id] = MarketItem(
            token_id,
            payable(msg.sender),
            payable(address(this)),
            price,
            false
        );

        _transfer(msg.sender, address(this), token_id);

        emit MarketItemCreated(
            token_id,
            msg.sender,
            address(this),
            price,
            false
        );
    }

    // Allows someone to resell a token they have purchased
    function resellToken(uint256 token_id, uint256 price) public payable
    {
        require(id_to_market_item[token_id].owner == msg.sender, "Only item owner can perform this operation");
        require(msg.value == listing_price, "Price must be equal to listing price");

        id_to_market_item[token_id].is_sold = false;
        id_to_market_item[token_id].price = price;
        id_to_market_item[token_id].seller = payable(msg.sender);
        id_to_market_item[token_id].owner = payable(address(this));
        _items_sold.decrement();

        _transfer(msg.sender, address(this), token_id);
    }

    // Creates the sale of a marketplace item
    // Transfers ownership of the item, as well as funds between parties
    function createMarketSale(uint256 token_id) public payable
    {
        uint price = id_to_market_item[token_id].price;
        address seller = id_to_market_item[token_id].seller;

        require(msg.value == price, "Please submit the asking price in order to complete the purchase");

        id_to_market_item[token_id].owner = payable(msg.sender);
        id_to_market_item[token_id].is_sold = true;
        id_to_market_item[token_id].seller = payable(address(0));

        _items_sold.increment();
        _transfer(address(this), msg.sender, token_id);

        payable(owner).transfer(listing_price);
        payable(seller).transfer(msg.value);
    }

    // Returns all unsold market items
    function fetchMarketItems() public view returns (MarketItem[] memory)
    {
        uint item_count = _token_ids.current();
        uint unsold_item_count = _token_ids.current() - _items_sold.current();
        uint current_index = 0;

        MarketItem[] memory items = new MarketItem[](unsold_item_count);

        for (uint i = 0; i < item_count; i++)
        {
            if (id_to_market_item[i + 1].owner == address(this))
            {
                uint curr_id = i + 1;
                MarketItem storage curr_item = id_to_market_item[curr_id];
                items[current_index] = curr_item;
                current_index += 1;
            }
        }

        return items;
    }

    // Returns only items that a user has purchased
    function fetchMyNFTs() public view returns (MarketItem[] memory)
    {
        uint total_item_count = _token_ids.current();
        uint item_count = 0;
        uint current_index = 0;

        for (uint i = 0; i < total_item_count; i++)
        {
            if (id_to_market_item[i + 1].owner == msg.sender)
            {
                item_count += 1;
            }
        }

        MarketItem[] memory items = new MarketItem[](item_count);

        for (uint i = 0; i < total_item_count; i++)
        {
            if (id_to_market_item[i + 1].owner == msg.sender)
            {
                uint curr_id = i + 1;
                MarketItem storage curr_item = id_to_market_item[curr_id];
                items[current_index] = curr_item;
                current_index += 1;
            }
        }

        return items;
    }

    // Returns only items that a user has listed
    function fetchItemsListed() public view returns (MarketItem[] memory)
    {
        uint total_item_count = _token_ids.current();
        uint item_count = 0;
        uint current_index = 0;

        for (uint i = 0; i < total_item_count; i++)
        {
            if (id_to_market_item[i + 1].seller == msg.sender)
            {
                item_count += 1;
            }
        }

        MarketItem[] memory items = new MarketItem[](item_count);

        for (uint i = 0; i < total_item_count; i++)
        {
            if (id_to_market_item[i + 1].seller == msg.sender)
            {
                uint curr_id = i + 1;
                MarketItem storage curr_item = id_to_market_item[curr_id];
                items[current_index] = curr_item;
                current_index += 1;
            }
        }

        return items;
    }
}

This contract inherits from the ERC721 standard implemented by OpenZepplin.

OpenZeppelin is an open-source platform for building secure dapps. The framework provides the required tools to create and automate Web3 applications.

Next, I was hoping to test out the core functionality of the smart contract code and environment, including:

  • Minting a token
  • Listing the token for sale
  • Selling the token to another user
  • Querying for tokens

Writing the test

To create the test locally, I replaced the sample code in test/sample-test.js with:

describe("FinalStatic", function()
{
    it("Should create and execute market sales", async function()
    {
        // Deploy the marketplace
        const FinalStatic = await ethers.getContractFactory("FinalStatic");
        const finalStatic = await FinalStatic.deploy();
        await finalStatic.deployed();

        let listing_price = await finalStatic.getListingPrice();
        listing_price = listing_price.toString();

        const auction_price = ethers.utils.parseUnits('1', 'ether');

        // Create 2 tokens
        await finalStatic.createToken("https://mytokenlocation1.com", auction_price, { value: listing_price });
        await finalStatic.createToken("https://mytokenlocation2.com", auction_price, { value: listing_price });

        const [_, buyer_address] = await ethers.getSigners();

        // Execute sale of token to another user
        await finalStatic.connect(buyer_address).createMarketSale(1, { value: auction_price });

        // Resell a token
        await finalStatic.connect(buyer_address).resellToken(1, auction_price, { value: listing_price });

        // Query for and return the unsold items
        items = await finalStatic.fetchMarketItems();

        items = await Promise.all(items.map(async i => {
            const token_URI = await finalStatic.tokenURI(i.token_id)

            let item = {
                price: i.price.toString(),
                token_id: i.token_id.toString(),
                seller: i.seller,
                owner: i.owner,
                token_URI
            };

            return item;
        }))

        console.log("items: ", items);
    })
});

Running the test

To start the test, I ran this command on Terminal:

npx hardhat test

Here’s the result I got:

image-center

From the log, we can see an array which contains the two marketplace placeholder items — indicating a successful test.

That’s great progress with the backbone so far.

Let’s jump over to the client side in the next post, where I will start building out the front end.

Categories: final-static