🧫 Building on Ethereum in 2020

[first draft] of a hackathon toolkit focused on fast iterations

👩‍🚀📡 [ UPDATE — May 15, 2020 ]: For an easier, more up-to-date guide, check out: https://medium.com/@austin_48503/programming-decentralized-money-300bacec3a4f

👩‍🚀📡 [ UPDATE — May 1, 2020 ]: The best way to get started with this dapp development stack is to fork 🏗 scaffold-eth. It abstracts away a lot of the installation and setup that this guide covers.

🧪 This is an experiment to try to make a super approachable “Create Ethereum App” toolkit to spark more tinkering on new products from new developers!

☢️ [Warning]: This tutorial is hard mode. I’m publishing it just so I have it around and others can skim it at their own risk. It is basically me documenting my journey to build 🚀easy mode.

First, you’ll need NodeJS>10, Git, and Yarn installed. Run node --version to see:

Image for post
Image for post

💡 If you have problems setting up your Node environment get help here.

Then we can jump off with create-eth-app:

yarn config set workspaces-experimental trueyarn create eth-app my-eth-app --template uniswapcd my-eth-appyarn react-app:start

This sets us up with a project directory my-eth-app and fires up a React app.

Image for post
Image for post

If you are just creating a frontend, drop your contracts in packages/contracts and you are off to the races: http://localhost:3000. (You can skip to the Frontend section below.)

Image for post
Image for post

🔥 Keep an eye on the Create Eth App community because I think it’s really warming up!

This also brings in the 🦄 Uniswap contracts and gives us some hints on interfacing with the contracts in ⚛️ React.

This sets us up to use 🔨 Ethers.js and also leverages 🛰 TheGraph.

🧐 Our goal is to create a smart contract backend and frontend at the same time: a hot reloading app on contract edit.

Buidler is a task runner for Ethereum. We’ll use it for all our low level smart contract compiling, debugging, and orchestration. From a new terminal, let’s install Buidler in a new folder named: my-eth-app/packages/buidler :

cd my-eth-app
cd packages
mkdir buidler
cd buidler
yarn init -y
yarn add --dev @nomiclabs/buidler @nomiclabs/buidler-truffle5 @nomiclabs/buidler-web3 web3

🔥 The Buidler community is also blossoming. It is easily extensible but what’s more important is it has an EVM designed for debugging.

We bring in the format of 🐷 Truffle and 🧰 Web3.js because if you google for help, you’ll likely find them. We want this tutorial to be as generic as possible so it might be heavier than a production stack. For example, many developers prefer to use 🧇 Waffle and 🔨 Ethers.js: read more about that here.

🔗 Here is a great example of a Buidler+Ethers+Waffle+TypeScript stack.

Fire up Buidler and ask it to create an empty buidler.config.js:

cd my-eth-app/packages/buidlernpx buidler
Image for post
Image for post

This buidler.config.js is where we will have all our smart contract orchestration logic; compiling, deploying, debugging, testing, and publishing.

The config file will start empty as module.exports = {} and we’ll add to it as we go.

Now that we have Builder installed, let’s use it to start a local blockchain:

cd my-eth-app/packages/buidlernpx buidler node
Image for post
Image for post

🎥 WTF is a private key?

Our local test chain is available at http://localhost:8545 for RPC calls and it comes with a bunch of test accounts loaded with test ether!

This implementation of the EVM is created with debugging in mind. We will see lots of feedback in this terminal.

Just like in the Buidler getting started guide, let’s make sure we can list our accounts. Open up your favorite code editor and let’s create an accounts() function in the my-eth-app/packages/buidler/buidler.config.js file:

const { usePlugin } = require('@nomiclabs/buidler/config')
usePlugin("@nomiclabs/buidler-truffle5");
task("accounts", "Prints the list of accounts", async () => {
const accounts = await web3.eth.getAccounts();
for (const account of accounts) {
console.log(account);
}
});
module.exports = {
defaultNetwork: 'localhost',
networks: {
localhost: {
url: 'http://localhost:8545',
/*
accounts: {
mnemonic: "SOME MNEMONIC TEXT HERE"
},*/
},
},
solc: {
version : "0.6.6",
optimizer: {
enabled: true,
runs: 200
}
}
}

We also added some config at the bottom to allow us to control our compile better and switch between networks. Notice the mnemonic option, this gives us the ability to switch to a wallet we control and not use the accounts built into the chain because we won’t have those in production.

Now let’s try it out and see if we can get a list of local accounts:

cd my-eth-app/packages/buidlernpx buidler accounts
Image for post
Image for post

Let’s create a balance() function in the buidler.config.js :

task("balance", "Prints an account's balance")
.addPositionalParam("account", "The account's address")
.setAction(async (taskArgs) => {
const balance = await web3.eth.getBalance(taskArgs.account)
console.log(web3.utils.fromWei(balance, "ether"), "ETH");
});

Then let’s ask for the balance of the first account from the list above:

npx buidler balance 0xc783df8a850f42e7F7e57013759C285caa701eB6
Image for post
Image for post

One helpful function to add is a send() in the buidler.config.js

task("send", "Send ETH")
.addParam("from", "From address or account index")
.addOptionalParam("to", "To address or account index")
.addOptionalParam("amount", "Amount to send in ether")
.addOptionalParam("data", "Data included in transaction")
.addOptionalParam("gasPrice", "Price you are willing to pay in gwei")
.addOptionalParam("gasLimit", "Limit of how much gas to spend")
.setAction(async (taskArgs) => {
let to
if(taskArgs.to){
to = taskArgs.to
}
let txparams = {
from: taskArgs.from,
to: to,
value: web3.utils.toWei(taskArgs.amount?taskArgs.amount:"0", "ether"),
gasPrice: web3.utils.toWei(taskArgs.gasPrice?taskArgs.gasPrice:"1.001", "gwei"),
gas: taskArgs.gasLimit?taskArgs.gasLimit:"24000"
}
if(taskArgs.data !== undefined) {
txparams['data'] = taskArgs.data
}
return new Promise((resolve, reject) => {
web3.eth.sendTransaction(txparams,(error, transactionHash) => {
console.log(`Error: ${error} : transactionHash: ${transactionHash}`)
})
})
});

And let’s make sure we can send ether from one account to another:

npx buidler send --from 0xc783df8a850f42e7F7e57013759C285caa701eB6 --to 0xeAD9C93b79Ae7C1591b1FB5323BD777E86e150d4 --amount 1
Image for post
Image for post
💡 Check the balance again to make sure it has changed.
Image for post
Image for post
🛠 Try this eth.build to double-check your local balances

💾 Grab this up-to-date buidler.config.jsgist

It includes extra helper functions like the addr() wrapper so you can (optionally) refer to accounts by their index in npx accounts:

npx buidler balance 1

npx buidler send --from 0 --to 1 --amount 10

🧑‍🏭 At this point we have a test chain, we have accounts, and we have test ether. We are ready to write our contracts, test them, and get them deployed. Let’s create a few different directories to keep things organized:

cd my-eth-app/packages/buidlermkdir contracts test scripts

Let’s create our first smart contract MyContract.sol at my-eth-app/packages/buidler/contracts/MyContract.sol with a simple owner mechanic, just to get us started:

pragma solidity >=0.5.0 <0.7.0;import "@nomiclabs/buidler/console.sol";contract MyContract {  address public owner;  constructor() public {
owner = msg.sender;
console.log("MyContract is owned by:",owner);
}

}

💡 See that console.log ? This is not Javascript here, it’s Solidity.

Now we can compile our contracts with:

cd my-eth-app/packages/buidlernpx buidler compile
Image for post
Image for post

This creates an “artifact” for your contract and if you are curious, you can inspect the file with cat artifacts/MyContract.json . It includes the ABI (information about how to talk to your contract) and the Bytecode (compiled code to deploy).

The scripts folder will contain all of your orchestration. Right now, let’s put together a scripts/deploy.js so we can automate the deploying of our contracts.

const fs = require('fs');
const chalk = require('chalk');
async function main() {
const MyContract = artifacts.require("MyContract");
const myContract = await MyContract.new();
console.log("MyContract deployed to:", chalk.green(myContract.address));
fs.writeFileSync("artifacts/MyContract.address",myContract.address)
// put your MetaMask account here so you will own the contract:
//let newOwner = "0x9c5aA9a9EcC961a1AFd2EE8ef3798ea5094b6c0E"
//console.log("Updating owner to "+chalk.magenta(newOwner))
//await myContract.updateOwner(newOwner)
// this is for after you make your contract a token:
//console.log("Sending some tokens to the new owner too...")
//await myContract.transfer(newOwner,""+10*10**18)
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});

💡 Note that we are going to keep track of the address of the contract. We will keep this in our artifacts folder so we can inject it into our frontend.

💾 Here is a more generic deploy script for later projects

Now let’s run our deploy script with:

npx buidler run scripts/deploy.js
Image for post
Image for post

We can also see the console.log over on our test chain output:

Image for post
Image for post

✨ Take a minute to bask in the glory of console.log in a smart contract! ✨

As your smart contract evolves you will want to write some tests for it. Let’s just write a quick example test test/myTest.js and save it for later:

const MyContract = artifacts.require("MyContract");
describe("My Dapp", function() {
let accounts;
let myContract;
before(async function() {
accounts = await web3.eth.getAccounts();
});
describe("My Contract", function() {
it("Should deploy my contract", async function() {
myContract = await MyContract.new();
});
describe("owner()", function() {
it("Should have an owner equal to the deployer", async function() {
assert.equal(await myContract.owner(), accounts[0]);
});
});
});
});

Now let’s run our tests to make sure the contract compiles and runs correctly:

npx buidler test
Image for post
Image for post

💡 It’s a good idea to change a couple things in the smart contract that should cause the tests to fail and then run npx buidler test again. 🤡 Test your tests!?

It’s important that whenever we deploy our contracts, we inject the new artifacts into our frontend. We want to be able to iterate on our UI and our contract logic in parallel. Let’s create a script for publishing our artifacts. Create a new file at my-eth-app/packages/buidler/scripts/publish.js :

const fs = require('fs');
const chalk = require('chalk');
const bre = require("@nomiclabs/buidler");
const contractDir = "./contracts"
async function main() {
const publishDir = "../react-app/src/contracts"
if (!fs.existsSync(publishDir)){
fs.mkdirSync(publishDir);
}
let finalContractList = []
fs.readdirSync(contractDir).forEach(file => {
if(file.indexOf(".sol")>=0){
let contractName = file.replace(".sol","")
console.log("Publishing",chalk.cyan(contractName), "to",chalk.yellow(publishDir))
try{
let contract = fs.readFileSync(bre.config.paths.artifacts+"/"+contractName+".json").toString()
let address = fs.readFileSync(bre.config.paths.artifacts+"/"+contractName+".address").toString()
contract = JSON.parse(contract)
fs.writeFileSync(publishDir+"/"+contractName+".address.js","module.exports = \""+address+"\"");
fs.writeFileSync(publishDir+"/"+contractName+".abi.js","module.exports = "+JSON.stringify(contract.abi));
fs.writeFileSync(publishDir+"/"+contractName+".bytecode.js","module.exports = \""+contract.bytecode+"\"");
finalContractList.push(contractName)
}catch(e){console.log(e)}
}
});
fs.writeFileSync(publishDir+"/contracts.js","module.exports = "+JSON.stringify(finalContractList))
}
main().then(() => process.exit(0)).catch(error => {console.error(error);process.exit(1);});

Now we can publish whenever we want to update our contracts in our app:

npx buidler run scripts/publish.js
Image for post
Image for post

🧐 This publish method puts the contracts in ./packages/react-app/src/contracts but I’m wondering if we can match the create-eth-app format better and put them in ./packages/buidler somewhere?

📱Frontend

Now that our contract is getting injected into the React app, we are ready to start iterating on our frontend. We created a boilerplate with create-eth-app using the Uniswap template and now we can start adding some components.

Our app should already be up and running with yarn react-app:start(from the very first set of commands we ran):

Image for post
Image for post

Open up my-eth-app/packages/react-app/src/App.js in your favorite code editor and we’ll start hacking on our app.

🔏 Web3 Provider: Most apps will use 📡 Infura or their own full nodes as a provider (reading from the blockchain) and then a chrome extension as the signer (injected wallet). Traditionally this was just 🦊 MetaMask but now we can leverage communities like Web3Modal or web3-react.

Let’s create a mainnetProvider in our App() that is always connected to mainnet using Infura (backup failover is Etherscan) with Ethers.js.

Throw this first line up near the top of the App.js file:

const mainnetProvider = ethers.getDefaultProvider("mainnet")

Now we can use this connection to read from the main Ethereum network.

💡 The contracts that come with the create-eth-app template can be interfaced with using the mainnetProvider. (We used Uniswap in this example.)

We will also need to talk to our own local Buidler node while we are working on our smart contracts. Therefore, we will also need a localProvider that will be pointed to http://localhost:8545 when we are working locally, but then switch to the main network once our contracts go live. Right after the mainnetProvider in App(), let’s add in this code to check our environment:

const FORCEMAINNET = false;
let localProvider = mainnetProvider
let LOCALHOST = false
if (!FORCEMAINNET && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1")){
localProvider = new ethers.providers.JsonRpcProvider("http://localhost:8545")
LOCALHOST = true;
}

🧐 Maybe this should actually just be in the .env file? Then for local or stage or production you have a .env file?

🔄 Poller: I always find myself needing to know up-to-date balances, the block number, the price of ETH, the price of gas, etc. Let’s create something that will poll for these things on a regular interval and update our state.

Let’s create my-eth-app/packages/react-app/src/Poller.js :

import React, { useEffect, useRef } from 'react';
export default function Poller(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
//run at start too
useEffect(() => {
callback()
}, []);
}

Then, if we import that into our packages/react-app/src/App.js with:

import Poller from "./Poller.js";

💡 Make sure all your import statements are above everything else.

We can do this to poll for different variables like our balance or the block. Add this snippet of code inside your App() :

const [blockNumber, setBlockNumber] = useState();
Poller(async ()=>{
let nextBlockNumber = await localProvider.getBlockNumber()
if(nextBlockNumber!=blockNumber){
setBlockNumber(nextBlockNumber)
console.log("BLOCK #",nextBlockNumber)
}
},1777)

💁‍♂️ Whoops, sorry, you’ll also need to bring in React’s useState for this to work:

import React, { useState, useEffect, useRef } from 'react'

If you venture into your browser’s console, you’ll see new local blocks:

Image for post
Image for post

Let’s use the Uniswap contracts and the example that is already in App.js to get the price of ETH and keep it in the state too. Copy and paste this in below your last poller:

const [price, setPrice] = useState();
Poller(async () => {
const ethDaiExchangeContract = new ethers.Contract(
addresses[MAINNET_ID].exchanges["ETH-DAI"],
abis.exchange,
mainnetProvider,
);
// A pre-defined address that owns some cDAI tokens
let exchangeRate = await ethDaiExchangeContract.getEthToTokenInputPrice("1000000000000000000"); // price of 1 Ether in DAI
exchangeRate = ((exchangeRate.div("10000000000000000").toNumber())/100).toFixed(2)
if(exchangeRate!=price){
setPrice(exchangeRate)
console.log("PRICE $"+exchangeRate)
}
},19777)
Image for post
Image for post

We’ll get the current gasPrice from ETH Gas Station using Axios:

cd my-eth-app/packages/react-appyarn add axios

Then bring Axios into your packages/react-app/src/App.js:

import axios from 'axios';

Then we bring in another Poller and use axios to get the current price of gas on the network. We’ll keep track of it in the app state as gasPrice:

const [gasPrice, setGasPrice] = useState();
const loadGasPrice = async () => {
axios.get('https://ethgasstation.info/json/ethgasAPI.json')
.then(function (response) {
let newGasPrice = response.data.fast*0.1
if(newGasPrice!=gasPrice){
console.log("GAS ",newGasPrice,"gwei")
setGasPrice(newGasPrice);
}
})
.catch(function (error) {
console.log(error);
})
}
Poller(loadGasPrice,39999)

Now we can display these in the render somewhere:

<div style={{position:'fixed',textAlign:'left',left:0,top:0,padding:10}}>
<div>
Block: #{blockNumber}
</div>
<div>
ETH Price: ${price}
</div>
<div>
Gas Price: {gasPrice} gwei
</div>
</div>
Image for post
Image for post

For a little user experience 🎂 icing on the cake, we want users to get an Ethereum account as soon as they land on our app. (Instant onboarding without needing MetaMask or understanding seed phrases.) This is like a session wallet that can be used for quick transactions from an ephemeral private key. To do this, we’ll bring in the burner-provider:

cd my-eth-app/packages/react-appyarn add burner-provider

Then bring it into our app:

import BurnerProvider from 'burner-provider';

💡 You might need to restart your react dev server as you install new dependencies.

We will track an injectedProvider (like MetaMask) but before anything is injected we will generate a new session wallet for the user. Throw this below our other pollers in our App() function:

const [injectedProvider, setInjectedProvider] = useState();
const createBurnerIfNoAccount = () => {
if (!injectedProvider){
if(localProvider.connection && localProvider.connection.url){
setInjectedProvider(new ethers.providers.Web3Provider(new BurnerProvider(localProvider.connection.url)))
}else{
setInjectedProvider(new ethers.providers.Web3Provider(new BurnerProvider(mainnetProvider.providers[0].connection.url)))
}
}else{
pollInjectedProvider()
}
}
React.useEffect(createBurnerIfNoAccount, [injectedProvider]);

The app will fail until we create a poller for the injectedProvider:

const [account, setAccount] = useState();
const pollInjectedProvider = async ()=>{
if(injectedProvider){
let accounts = await injectedProvider.listAccounts()
if(accounts && accounts[0] && accounts[0]!=account){
console.log("ACCOUNT: ",accounts[0])
setAccount(accounts[0])
}
}
}
Poller(pollInjectedProvider,1999)

Once we have the account we can poll its balance too:

const [balance, setBalance] = useState(0);
const pollBalance = async ()=>{
if(account&&localProvider){
let newBalance = await localProvider.getBalance(account)
newBalance = ethers.utils.formatEther(newBalance)
if(newBalance!=balance){
console.log("NEW BALANCE:",newBalance,"Current balance",balance)
setBalance(newBalance)
}
}
}
Poller(pollBalance,777)
Image for post
Image for post

Real fast, we need to install a must-have for displaying addresses. It’s called a Blockie. These help visually verify accounts (mostly 😅) and are common across many existing applications. Let’s install:

cd my-eth-app/packages/react-appyarn add react-blockies

Import the package into our app:

import Blockies from 'react-blockies';

Finally, we can display the injected/session account and its balance in local test ETH in the top right corner. Throw this in your render:

<div style={{position:'fixed',textAlign:'right',right:0,top:0,padding:10}}>
{account?(
<div>
<Blockies seed={account.toLowerCase()}/> {account}
<div>
Balance: {balance} ETH
</div>
</div>
):"Connecting..."}
</div>

You can load your app (http://localhost:3000) in a few different incognito windows to see that a new Ethereum account is created for each session and stored in local storage:

Image for post
Image for post
Incognito Session 1
Image for post
Image for post
Incognito Session 2

Let’s test out that balance display by sending some test ether to our account:

cd my-eth-app/packages/buidlernpx buidler send --from 0xc783df8a850f42e7f7e57013759c285caa701eb6 --amount 1.234 --to **ADDRESS**
Image for post
Image for post

Now let’s get Web3Modal installed so our users can upgrade to a more secure wallet for safer signing and sending of transactions. This is MetaMask or whatever injected web3 chrome extensions you have plus others:

cd my-eth-app/packages/react-appyarn add web3modal
yarn add @walletconnect/web3-provider

yarn add fortmatic
yarn add @toruslabs/torus-embed
yarn add authereum

(The bottom three commands here are optional. You can pick and choose your optional providers.)

Then we can instantiate the Web3Modal with the optional providers. First, bring in the imports that you want:

import Web3Modal from "web3modal";
import WalletConnectProvider from "@walletconnect/web3-provider";
import Fortmatic from "fortmatic";
import Torus from "@toruslabs/torus-embed";
import Authereum from "authereum";

Then, put this at the top of your code after the imports:

const web3Modal = new Web3Modal({
//network: "mainnet", // optional
cacheProvider: true, // optional
providerOptions: {
walletconnect: {
package: WalletConnectProvider, // required
options: {
infuraId: "2717afb6bf164045b5d5468031b93f87" // MY INFURA_ID, SWAP IN YOURS!
}
},
fortmatic: {
package: Fortmatic, // required
options: {
key: "FORTMATIC_KEY" // required
}
},
torus: {
package: Torus, // required
options: {}
},
authereum: {
package: Authereum, // required
options: {}
}
}
});

🚒 You will need to put in your API keys for most of these services including a valid Infura ID instead of this one!

We need to create a function that will get and set the injected provider in the app state. Put this code in your App() function after all the pollers:

const loadWeb3Modal = async ()=>{
const provider = await web3Modal.connect();
console.log("GOT CACHED PROVIDER FROM WEB3 MODAL",provider)
setInjectedProvider(new ethers.providers.Web3Provider(provider))
pollInjectedProvider()
}

Might as well have a logout function that clears things:

const logoutOfWeb3Modal = async ()=>{
const clear = await web3Modal.clearCachedProvider();
window.location.reload()
}

Now let’s add aConnect button to our account display in the top right that will open up Web3Modal and a logout button for after they are connected.

let modalButtons = []
if (web3Modal.cachedProvider) {
modalButtons.push(
<button onClick={logoutOfWeb3Modal}>logout</button>
)
}else{
modalButtons.push(
<button onClick={loadWeb3Modal}>connect</button>
)
}

(Then, put {modalButtons} in that top right div of the render.)

Image for post
Image for post

Also, let’s check on page load if we can connect to a cached provider. Put this anywhere in the App() function:

React.useEffect(async () => {
if (web3Modal.cachedProvider) {
loadWeb3Modal()
}
}, []);

Now our users will have an account on page load but can upgrade to a more secure wallet like a browser extension for larger transactions.

Image for post
Image for post
Logging in from a session/burner account into a more secure MetaMask account.

📥 Contract Loader: We are injecting our contracts using the publish script from Buidler. We want them to load into our React state automatically. Let’s create a new ContractLoader component at my-eth-app/packages/react-app/src/ContractLoader.js :

import { ethers } from "ethers";
export default function ContractLoader(provider,ready) {
let contractList = require("./contracts/contracts.js")
for(let c in contractList){
let contracts = []
contracts[contractList[c]] = new ethers.Contract(
require("./contracts/"+contractList[c]+".address.js"),
require("./contracts/"+contractList[c]+".abi.js"),
provider,
);
if(typeof ready == "function") { ready(contracts) }
}
}

We import it the contract loader into our app:

import ContractLoader from "./ContractLoader.js";

Then we can invoke the ContractLoader() when our app fires up. Put this code with your other useEffect() functions that run on load:

const [contracts, setContracts] = useState();
React.useEffect(() => {
//localProvider.resetEventsBlock(0)
ContractLoader(localProvider, async (loadedContracts)=>{
console.log("CONTRACTS ARE READY!",loadedContracts)
setContracts(loadedContracts)
// listen to events after contracts are loaded
//listenForEvents(loadedContracts)
})
},[])

The browser’s console will now display the interface with the smart contract and give us a chance to run anything once the contracts are ready:

Image for post
Image for post

We can run a Poller to ask MyContract who the owner is:

const [owner, setOwner] = useState();
const loadOwner = async ()=>{
if(contracts){
let newOwner = await contracts.MyContract.owner()
if(newOwner!=owner){
console.log("OWNER: ",newOwner)
setOwner(newOwner)
}
}
}
Poller(loadOwner,3333)

Now let’s display our contract address and the owner. Throw this code in right after the last stuff:

let contractStatus = "Connecting to MyContract..."
let ownerStatus = "Loading Owner..."
if(contracts){
contractStatus = (
<div>
MyContract is deployed to <Blockies seed={contracts.MyContract.address.toLowerCase()}/> {contracts.MyContract.address}
</div>
)
if(owner){
ownerStatus = (
<div>
MyContract is owned by <Blockies seed={owner.toLowerCase()}/> {owner}
</div>
)
}
}
let contractDisplay = (
<div style={{position:'fixed',textAlign:'left',left:0,bottom:20,padding:10}}>
{contractStatus}
{ownerStatus}
</div>
)

Then we can add this to our render somewhere:

<div style={{position:'fixed',textAlign:'left',left:0,bottom:20,padding:10}}>
{contractStatus}
{ownerStatus}
</div>
Image for post
Image for post

✨ This is a key moment for this development stack. We can make small, incremental changes to our smart contract as we build our frontend UI. ✨

Now, back to my-eth-app/packages/buidler/contracts/MyContract.sol. Let’s add a new function to our contract that enables the owner to give control to someone else:

function updateOwner(address newOwner) public {
require(msg.sender == owner, "NOT THE OWNER");
owner = newOwner;
console.log("Updated Owner of ",address(this)," to ",owner);
}

Now we can run through the Buidler dance to compile and deploy:

cd my-eth-app/packages/buidlernpx buidler compile
npx buidler run scripts/deploy.js
npx buidler run scripts/publish.js

Keep an eye on your app when this script finishes, because it will reload. 😉

Image for post
Image for post
👀 Peep our app hot reloading as we redeploy the contract a couple times.

If we add (uncomment) a few lines in our buidler/scripts/deploy.js file:

let newOwner = "***YOUR ADDRESS FROM YOUR FRONTEND GOES HERE***"
console.log("Updating owner to "+chalk.magenta(newOwner))
await myContract.updateOwner(newOwner)
Image for post
Image for post

Now our script will automatically set a new owner on deploy. (Replace this address with one of your frontend addresses) Then, run:

npx buidler run scripts/deploy.js
npx buidler run scripts/publish.js
Image for post
Image for post
💡 Notice the owner of the contract and the frontend address are the same now!

As our smart contracts become more advanced we will want to stand on the shoulders of giants. The OpenZeppelin contracts have been audited and provide easy access to well known standards:

cd my-eth-app/packages/buidleryarn add @openzeppelin/contracts@3.0.0-rc.1

And now we can turn our contract into an ERC20 token with a few lines:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

And inherit that contract by changing this line to:

contract MyContract is ERC20 {

And our constructor should probably name and mint some MyToken:

constructor() public ERC20("MyToken", "MT") {
_mint(msg.sender,10000 * (10 ** 18));
owner = msg.sender;
console.log("MyContract is owned by:",owner);
}

Let’s run our compile again and make sure our syntax is solid:

cd my-eth-app/packages/buidlernpx buidler compile
Image for post
Image for post
💁‍♀️ Ignore the warnings, it’s all compiling! 😅

Let’s setup our scripts/deploy.js to send some tokens to our frontend user:

console.log("Sending some tokens to the new owner too...")
await myContract.transfer(newOwner,""+10*10**18)

Finally, we can compile, deploy, and publish our new token contract:

npx buidler run scripts/deploy.js
npx buidler run scripts/publish.js
Image for post
Image for post

In our app we can poll for our new token balance. Throw this with the rest of your pollers in App():

const [tokenBalance, setTokenBalance] = useState(0);
const pollTokenBalance = async ()=>{
if(account&&localProvider&&contracts){
let newTokenBalance = await contracts.MyContract.balanceOf(account)
newTokenBalance = ethers.utils.formatEther(newTokenBalance)
if(newTokenBalance!=tokenBalance){
console.log("NEW TOKEN BALANCE:",newTokenBalance,"Current token balance",tokenBalance)
setTokenBalance(newTokenBalance)
}
}
}
Poller(pollTokenBalance,779)

And then display the tokenBalance in the account div in the top right:

<div>
Tokens: {tokenBalance}
</div>
Image for post
Image for post

Now in our app we have tokens!

Image for post
Image for post

We’ll need to send some ETH to our injected/session account so we have gas to pay for the transaction:

npx buidler send --from 0xc783df8a850f42e7F7e57013759C285caa701eB6 --amount 1.234 --to *YOUR ADDRESS*
Image for post
Image for post

If you are using MetaMask or another chrome extension that controls the network for you, make sure you switch over to http://localhost:8545 :

Image for post
Image for post

You know everything is working if your account now has test ETH and tokens:

Image for post
Image for post

🏗 A little scaffolding UI might help with future projects using this stack.

Let’s build an ugly form that lets us call updateOwner on our smart contract. Throw this under the rest of your pollers:

const [newOwner, setNewOwner] = useState();
let newOwnerForm = (
<div style={{borderBottom:"1px solid #555555",paddingBottom:15}}>
Set new owner: <input type="text" value={newOwner} onChange={e => setNewOwner(e.target.value)} />
<button onClick={async ()=>{
let WritableMyContract = new ethers.Contract(
contracts.MyContract.address,
contracts.MyContract.interface.abi,
injectedProvider.getSigner()
);
let result = await WritableMyContract.updateOwner(newOwner)
console.log("Transaction Result:",result)
}}>go</button>
</div>
)

💡 Notice that the contracts.MyContract is a read-only contract interface and we have to build a WritableMyContract using our signer from the injectedProvider. (Find a better way to abstract this in a later version.)

Then put {newOwnerForm} in the top of the bottom left div:

Image for post
Image for post

Rad, let’s run through a test of setting a new owner using our current owner:

Image for post
Image for post
Using our ugly form and our owner (MetaMask account) we update the owner of our contract.

Now that we can read from, write to, and iterate on our smart contracts, let’s add a new function called broadcast() that lets the current owner send out a message using events. Add this to your buidler/contracts/MyContract.sol:

function broadcast(string memory message) public {
require(msg.sender == owner, "NOT THE OWNER");
emit Broadcast(owner, message);
}
event Broadcast(address newOwner, string message);

Compile, deploy, and publish:

npx buidler run scripts/deploy.js
npx buidler run scripts/publish.js
Image for post
Image for post

Now in our frontend we can listen for this event. Head back over to our app at my-eth-app/packages/react-app/src/App.js and put this code just above the ContractLoader in the App() function:

const [messages,setMessages] = useState([]);
const listenForEvents = (loadedContracts)=>{
console.log("LISTENING TO CONTRACTS!")
loadedContracts.MyContract.on("Broadcast", (sender, message) => {
let obj = {sender, message}
setMessages(messages => [...messages, obj])
});
}

Then, add (uncomment) this from the contract loader so we will start to listen to the events as soon as the contracts are loaded.

listenForEvents(loadedContracts)
Image for post
Image for post

Now we can create an ugly form to broadcast a message. Throw this code in the App() before the render return so we have a broadcastForm:

const [broadcast, setBroadcast] = useState();
let broadcastForm = (
<div>
Broadcast: <input type="text" value={broadcast} onChange={e => setBroadcast(e.target.value)} />
<button onClick={async ()=>{
let WritableMyContract = new ethers.Contract(
contracts.MyContract.address,
contracts.MyContract.interface.abi,
injectedProvider.getSigner()
);
let result = await WritableMyContract.broadcast(broadcast)
console.log("Transaction Result:",result)
}}>go</button>
</div>
)

Then add {broadcastForm} to your bottom left div above the set owner stuff:

<div style={{position:'fixed',textAlign:'left',left:0,bottom:20,padding:10}}>
{broadcastForm}
{newOwnerForm}
{contractStatus}
{ownerStatus}
</div>
Image for post
Image for post

Now when we call broadcast() on our contract anyone in the decentralized world can receive our message by parsing events from the blockchain. 🌎 📠

Let’s display the messages. Put this under the rest of your pollers:

let messageDisplay = []
for(let m in messages){
messageDisplay.push(
<div>
<Blockies seed={messages[m].sender.toLowerCase()}/> {messages[m].message}
</div>
)
}

Then put this code in the render somewhere:

<div style={{position:'fixed',textAlign:'left',left:10,top:"20%",padding:10}}>
{messageDisplay}
</div>

If you are logged in as a MetaMask user, hit the logout button. Let’s proceed using the instant onboarding account just to demo the functionality:

Image for post
Image for post

We used our MetaMask account to set the owner of our contract to our “session account” and then we logged out. Now let’s send our session account some test ETH:

npx buidler send --from 0xc783df8a850f42e7f7e57013759c285caa701eb6 --amount 1.234 --to **YOUR_SESSION_ACCOUT**
Image for post
Image for post
Image for post
Image for post

Now let’s play around with this broadcast() function on our contract:

Image for post
Image for post
💡Remember, you have to use the account that owns the contract and has test ETH to broadcast.

You’ll notice that your app is only loading the most recent events if you reload. Events on Ethereum are handy for writing “cheap” data to the blockchain when a contract doesn’t need to read the data. However, it is pretty clunky because you have crawl through all the blocks looking for events. With version 4 of ethers.js you can resetEventsBlock(0) and that will set the start point for parsing events.

💡 For some reason 0 doesn’t work here but 10 does (probably something weird with events and the npx buidler node because the same behavior happens in this eth.build. (Maybe it has to be the block the contract was deployed?)

Let’s reset the events block to 10. Add (uncomment) this code in your useEffect right above the ContractLoader:

localProvider.resetEventsBlock(10)
Image for post
Image for post

Now you can see all of the messages, but beware, this will be really slow in production if it has to look back through tens of thousands of blocks.

Image for post
Image for post

As we can see, parsing events from our contract might work on a small scale, but we are going to run into long loading times as our app grows and the blockchain gets deeper. Luckily, there is a service called The Graph that can do the parsing for you and provide a GraphQL interface to your data.

If you take a look at my-eth-app/packages/react-app/src/index.js we can see the Apollo client:

const client = new ApolloClient({
uri: "https://api.thegraph.com/subgraphs/name/graphprotocol/uniswap",
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root"),
);

We can see it is connecting to the Uniswap subgraph. (⬅️ Click that link to play around with it!)

Let’s craft a query. The example in the template just gets the first 5 exchanges. Let’s tweak that query a little to sort the list by tradeVolumeUSD in descending order. Find GET_EXCHANGES in your App.js and update the code to:

const GET_EXCHANGES = gql`
{
exchanges(first: 5, orderBy: tradeVolumeUSD, orderDirection: desc) {
id
tokenAddress
tokenSymbol
tokenName
tradeVolumeUSD
}
}
`;

The code to pass the query to useQuery is already ready:

const { loading, error, data } = useQuery(GET_EXCHANGES);React.useEffect(() => {
if (!loading && !error && data && data.exchanges) {
console.log({ exchanges: data.exchanges });
}
}, [loading, error, data]);

Now in our app we can build a display of each exchange and their volume. Throw this in App.js below the useEffect above:

let exchangeDisplay = []
if(data&&data.exchanges){
for(let d in data.exchanges){
exchangeDisplay.push(
<div>
{data.exchanges[d].tokenSymbol} ${Math.round(data.exchanges[d].tradeVolumeUSD/1000000)}M
</div>
)
}
}

Finally, let’s throw this in your render somewhere and we can see exchange volume on the right of our app:

<div style={{position:'fixed',textAlign:'right',right:10,top:"20%",padding:10}}>
{exchangeDisplay}
</div>
Image for post
Image for post

💡 If you would like to play around with The Graph more, try running yarn create eth-app my-eth-app without any template (We use Uniswap for this build). It sounds confusing, but with no template, you will have a full subgraph that is custom to the project. (Instead of the Uniswap template)

💅 As a final touch let’s add in a Graph and Buidler link to the “Learn” section so we have easy links for reference:

<a className="App-link" href="https://thegraph.com/docs/introduction" target="_blank" rel="noopener noreferrer">
Learn Graph
</a>
<a className="App-link" href="https://buidler.dev/getting-started/#quick-start" target="_blank" rel="noopener noreferrer">
Learn Buidler
</a>
Image for post
Image for post

This was just too massive of an undertaking for one tutorial. After running through it on a few machines it was also hard to follow on different platforms. Eventually I moved all the hooks to their own library but that made them hard to edit on the fly. Finally, I ended up taking this stack and improving it and making it available as 🏗 scaffold-eth.

💬 Hit me up with feedback as always: @austingriffith on the socials!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store