-
Print
-
DarkLight
-
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
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.
- Ethereum
- Solidity - The language for implementation of smart contracts
- Open Zepellin - Smart Contract library to minimize risk
- Web3 - Javascript library collection to interact with ethereum nodes
- Truffle - for contract testing and pipeline (for deployment of smart contracts)
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 anencrypted_private_key
. Because of this, please use best practices with minimizing risk and never share any private keys or mnemonic phrases!
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 { }
}