Blockchain
  • 15 Oct 2021
  • 14 Minutes to read
  • Contributors
  • Dark
    Light
  • PDF

Blockchain

  • Dark
    Light
  • PDF

Blockchain

Chainstarters has thoughtfully crafted a platform that gives the developer full flexibility.

This means our tools are not only meant to develop full-stack applications with a distributed infrastructure, but to include the benefits of decentralized Web 3.0.

When launching a project in Chainstarters, the Dashboard includes a Blockchain UI. Let's take a look:

Dashboard View

blockchain-view

The chainstarters dashboard gives you a minting contract (which is also in your source code).

Resources

Before we continue, you may want to read and/or review some resources on Ethereum, Smart Contracts, Open Zepellin, Web3, and Truffle.

The chainstarters platform leverages these libraries and maintains them so that projects generated on the platform be free of concerns with regards to risk and time spent in writing contracts and interfaces.

Smart Contracts

Back to the Blockchain Contracts view in the Chainstarters console. The smart contracts you see here are the base mining contract (ERC20 standard) and a Migrations contract (which allows for contract deployment, which we will explain later in this guide).

To dive deeper into what is available to you, let's take a look at the API code that's provided for you. If you need reference on how to find and bring this code into your local machine, please review the GraphQL API Guide in Getting Started

There are two areas of the code that we want to look at:

  • graphql/src/lib/eth/index.js
  • graphql/src/blockchain/

Web3

Let's first explore the Web3 interface that we spin up for you, which can be found in graphql/src/lib/eth/index.js

    const Web3 = require('web3')
    const config = require('../../config')

We initially bring in the Web3 library and the following from config.js

    module.exports {
        ...
        contract address,
        minting accounts,
        admin accounts
    }

The minting accounts are a minter per environment: dev and prod. These accounts as self described are to mint ERC20 standard tokens.

Web3 Methods

Currently, we have these methods in API

  • getBalance
  • walletSwap

We will have more methods available soon, but you can use this file to create additional methods as needed for your project, please refer to the Web3 link in the start of this guide for documentation. You'll also notice that knex is brought in since we do store public wallet addresses in the DB users table, and blockchain_admin and blockchain_minter tables which store addresses accordingly. To view the admin and minter tables, use a DB explorer such as TablePlus, which is available on Mac and Windows.

Important! The database also stores a public_key and an encrypted_private_key. Because of this, please use best practices with minimizing risk and never share any private keys or mnemonic phrases!

address-view-in-db

The ABI

The ABI (application binary interface) is a json object that you get from using a solidity compiler such as solc on the command line, but since we have the power of Truffle and Web3, we can output an ABI with every migration (deployment).

Using the ABI in Web3

The ABI is very important to you, since you will be using it in any web3 function that requires you to act on a contract, which most certainly will be the case.

In the web3 code that's included with your project you'll see that this is how get methods from our contract (example: getting the balance of a wallet address)

    const web3 = new Web3(new Web3.providers.HttpProvider(config.nodeAddress), null, { transactionConfirmationBlocks: 1 })
    const contract = new web3.eth.Contract(abi, config.contractAddress, { gasPrice: '0' })
    
    return contract.methods.balanceOf(wallet_address).call({ from: wallet_address })

Once we use the abi to unlock the contract methods, we can simply use contract.methods to access the methods available from the contract ABI you're importing.

Some examples of other methods

  • mint
  • balanceOf
  • freezeAccount
  • unfreezeAccount
  • addUser
  • changeUserMultiplier

For more information on Web3 methods, check out the docs.

Output the ABI

Here is a function that handles deployments specific to admins and minters. The thing to look for here is where we output the ABI

    module.exports = function(deployer) {
        const admins = new HDWalletProvider({
            privateKeys: admins_private_keys,
            providerOrUrl: NODE_ADDRESS,
        })
        const minters = new HDWalletProvider({
            privateKeys: minters_private_keys,
            providerOrUrl: NODE_ADDRESS,
        })
        deployer.then(async () => {
            const contract = await deployer.deploy(BaseContract, "NewContract5", "NC")
            
            // This will output new contract address.
            fs.writeFile('../../../contractAddress.json', JSON.stringify({ contract_address: contract.address }), (e) => {
                if (e) console.log(e)
            })
            
            // This will output new contract abi.
            fs.writeFile('../lib/eth/contractAbi.json', JSON.stringify({ abi: contract.abi }), (e) => {
                if (e) console.log(e)
            })
            
            // Take each minter and admin from secrets
            // and add them as such to the new contract.
            const minterRole = await contract.MINTER_ROLE()
            const adminRole = await contract.ADMIN_ROLE()
            
            for (minter of minters.addresses) {
                await contract.grantRole(minterRole, minter)
            }
            for (admin of admins.addresses) {
                await contract.grantRole(adminRole, admin)
            }
        })
    };

The point of the ABI is to see your contract object output in a far more readable format JSON. Converting binary to JSON will always be more readable. The ABI is so important in testing and verifying that your contracts are are doing what you intended.

Migrations

All contract migrations in the API are handled with this smart contract

    pragma solidity 0.8.0;

    contract Migrations {
      address public owner = msg.sender;
      uint public last_completed_migration;

      modifier restricted() {
        require(
          msg.sender == owner,
          "This function is restricted to the contract's owner"
        );
        _;
      }

      function setCompleted(uint completed) public restricted {
        last_completed_migration = completed;
      }
}

Here is the simple JavaScript function to intiate a contract deployment

       const Migrations = artifacts.require("Migrations");

        module.exports = function (deployer) {
            deployer.deploy(Migrations);
        };

Writing your own contracts

If you plan to add your own Ethereum Smart Contracts, we have included a complete suite of OpenZepellin contracts for you to use and/or leverage.

The way you would import these contracts into your contract looks something like this (importing Context.sol):

    contract <Your Context Contract> is Context {
        ...contract code 
    }

You'll also notice that a lot of the base contracts that are included from OpenZepellin start with abstract contract. The abstract contract allows for one or multiple functions without an implementation. This is perfectly acceptable for base contracts, which you will be importing into your contract and implementing the functions from them on your contract.

Do not write an abstract contract unless you intend to use it as a base contract. With Chainstarters, you shouldn't have to write any base contracts.

Contracts included:

  • Access
    • Ownable.sol
    • AccessControl.sol
  • Governance
    • TimelockController.sol
  • Metatx
    • ERC2771Context.sol
  • Proxy
    • Proxy.sol
    • UpgradeableProxy.sol
  • Security
    • Pausable.sol
    • ReentrancyGuard.sol
  • Token
    • ERC20.sol
    • ERC721.sol
    • ERC777.sol
    • ERC1155.sol
  • Utils
    • Cryptography
    • Escrow
    • Introspection
    • Math
    • Structs
    • Address.sol
    • Initializable.sol
    • Context.sol
      Also included are a complete set of Mock contracts as well as other contracts in all folders.

If you need a push into working with the OpenZepellin Library, review the documentation here.

If you click here, you will be directed to the Contracts section of the documentation which explains on how you can leverage the modularity of these smart contracts.

Finally, the OpenZepellin Forum is a great place to have your specific Smart Contract implementation questions answered.

Using Truffle Ganache for Testing

We recommend using Ganache for testing on your own local blockchain.

Web3 Method Examples for ERC721 Smart Contracts

Note these are basic example implementations. For full documentation go to the Web3js documentation here.

Important, while we do use base web3 in the starter Chainstarters project, you are able to import a library of your choice for contract creation and interaction. Keep in mind updates needed to your config file and any credential updates you may need.

Getting an NFT Owner

    function getOwner(token) {
        const contract = web3.eth.Contract(ERC721Contract.abi, ERC721Contract_address)
        const owner = contract.methods.ownerOf(token)
        
        return owner
    }

Getting an owner of an NFT is a core method to write for web3 interaction with an NFT contract.

When you are writing your methods for an ERC721 contract, be sure to leverage the included config.js and update your env with the contract address

ERC 721 Example

ERC 721 (or the popular term: NFT) is a smart contract representing a unique digital asset. The non-fungible function of the contract means it will always be maintain the same value regardless of transfer of ownership, or other environmental factors. Uses for ERC721 include authentication for collectibles, which is the current trendline, the asset could also represent stake in land, real estate, among other goods that would need certfication of authenticity to maintain value.

In a Chainstarters Project, you have access to the Open Zeppelin library which includes an ERC 721 smart contract, here is an example of the main contract available:

graphql/src/blockchain/openzeppelin-contracts/token/ERC721/ERC721.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "./extensions/IERC721Enumerable.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";

/**
 * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
 * the Metadata extension, but not including the Enumerable extension, which is available separately as
 * {ERC721Enumerable}.
 */
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
    using Address for address;
    using Strings for uint256;

    // Token name
    string private _name;

    // Token symbol
    string private _symbol;

    // Mapping from token ID to owner address
    mapping (uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping (address => uint256) private _balances;

    // Mapping from token ID to approved address
    mapping (uint256 => address) private _tokenApprovals;

    // Mapping from owner to operator approvals
    mapping (address => mapping (address => bool)) private _operatorApprovals;

    /**
     * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
     */
    constructor (string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return interfaceId == type(IERC721).interfaceId
            || interfaceId == type(IERC721Metadata).interfaceId
            || super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC721-balanceOf}.
     */
    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }

    /**
     * @dev See {IERC721-ownerOf}.
     */
    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "ERC721: owner query for nonexistent token");
        return owner;
    }

    /**
     * @dev See {IERC721Metadata-name}.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC721Metadata-symbol}.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0
            ? string(abi.encodePacked(baseURI, tokenId.toString()))
            : '';
    }

    /**
     * @dev Base URI for computing {tokenURI}. Empty by default, can be overriden
     * in child contracts.
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }

    /**
     * @dev See {IERC721-approve}.
     */
    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ERC721.ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");

        require(_msgSender() == owner || ERC721.isApprovedForAll(owner, _msgSender()),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _approve(to, tokenId);
    }

    /**
     * @dev See {IERC721-getApproved}.
     */
    function getApproved(uint256 tokenId) public view virtual override returns (address) {
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");

        return _tokenApprovals[tokenId];
    }

    /**
     * @dev See {IERC721-setApprovalForAll}.
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(operator != _msgSender(), "ERC721: approve to caller");

        _operatorApprovals[_msgSender()][operator] = approved;
        emit ApprovalForAll(_msgSender(), operator, approved);
    }

    /**
     * @dev See {IERC721-isApprovedForAll}.
     */
    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    /**
     * @dev See {IERC721-transferFrom}.
     */
    function transferFrom(address from, address to, uint256 tokenId) public virtual override {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");

        _transfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        _safeTransfer(from, to, tokenId, _data);
    }

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * `_data` is additional data, it has no specified format and it is sent in call to `to`.
     *
     * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
     * implement alternative mechanisms to perform token transfer, such as signature-based.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual {
        _transfer(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    /**
     * @dev Returns whether `tokenId` exists.
     *
     * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
     *
     * Tokens start existing when they are minted (`_mint`),
     * and stop existing when they are burned (`_burn`).
     */
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }

    /**
     * @dev Returns whether `spender` is allowed to manage `tokenId`.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ERC721.ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || ERC721.isApprovedForAll(owner, spender));
    }

    /**
     * @dev Safely mints `tokenId` and transfers it to `to`.
     *
     * Requirements:
     d*
     * - `tokenId` must not exist.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeMint(address to, uint256 tokenId) internal virtual {
        _safeMint(to, tokenId, "");
    }

    /**
     * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
     * forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
     */
    function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual {
        _mint(to, tokenId);
        require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    /**
     * @dev Mints `tokenId` and transfers it to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
     *
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - `to` cannot be the zero address.
     *
     * Emits a {Transfer} event.
     */
    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    /**
     * @dev Destroys `tokenId`.
     * The approval is cleared when the token is burned.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     *
     * Emits a {Transfer} event.
     */
    function _burn(uint256 tokenId) internal virtual {
        address owner = ERC721.ownerOf(tokenId);

        _beforeTokenTransfer(owner, address(0), tokenId);

        // Clear approvals
        _approve(address(0), tokenId);

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);
    }

    /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *  As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     *
     * Emits a {Transfer} event.
     */
    function _transfer(address from, address to, uint256 tokenId) internal virtual {
        require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
        require(to != address(0), "ERC721: transfer to the zero address");

        _beforeTokenTransfer(from, to, tokenId);

        // Clear approvals from the previous owner
        _approve(address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    /**
     * @dev Approve `to` to operate on `tokenId`
     *
     * Emits a {Approval} event.
     */
    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
    }

    /**
     * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
     * The call is not executed if the target address is not a contract.
     *
     * @param from address representing the previous owner of the given token ID
     * @param to target address that will receive the tokens
     * @param tokenId uint256 ID of the token to be transferred
     * @param _data bytes optional data to send along with the call
     * @return bool whether the call correctly returned the expected magic value
     */
    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
        private returns (bool)
    {
        if (to.isContract()) {
            try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
                return retval == IERC721Receiver(to).onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("ERC721: transfer to non ERC721Receiver implementer");
                } else {
                    // solhint-disable-next-line no-inline-assembly
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning.
     *
     * Calling conditions:
     *
     * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
     * transferred to `to`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` will be burned.
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual { }
}


Was this article helpful?

What's Next