CryptoZombie: App Front-ends & Web3.js
try it yourself
To create front end webpage for our DApp use Web3.js a js library.
JSON-RPC: language that ethereum nodes speak. Very ugly.
Install Web3.js(in terminal):
// Using NPM
npm install web3
// Using Yarn
yarn add web3
// Using Bower
bower install web3
Web3 Providers
Setting a Web3 Provider in Web3.js tells our code which node we should be talking to handle our reads and writes.
Infura: get data for users from blockchain without keeping a node as a developer
Infura is a service that maintains a set of Ethereum nodes with a caching layer for fast reads, which you can access for free through their API. Using Infura as a provider, you can reliably send and receive messages to/from the Ethereum blockchain without needing to set up and maintain your own node.
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
Metamask: manage user private key safely ->So make your program Metamask compatible!
detect to see if the user has Metamask installed:
window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have web3. Probably
// show them a message telling them to install Metamask in
// order to use our app.
}
// Now you can start your app & access web3js freely:
startApp()
})
Web3.js will need its address and its ABI to talk to your contract.
Contract Address:
After you deploy your contract, it gets a fixed address on Ethereum where it will live forever. (...) You'll need to copy this address after deploying in order to talk to your smart contract.
Contract ABI(Application Binary Interface):
Basically it's a representation of your contracts' methods in JSON format that tells Web3.js how to format function calls in a way your contract will understand.
The compiler will give ABI at deployment, so copy and save this as well.
Instantiating a Web3.js Contract:
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
Zombie example:
var cryptoZombies;
function startApp(){
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
}
Web3.js has two methods we will use to call functions on our contract: call and send.
Call
call is used for view and pure functions. It only runs on the local node, and won't create a transaction on the blockchain.
myContract.methods.myMethod(123).call()
Send
send will create a transaction and change data on the blockchain. You'll need to use send for any functions that aren't view or pure.
myContract.methods.myMethod(123).send()
Getting data
In Solidity, when you declare a variable public, it automatically creates a public "getter" function with the same name. So if you wanted to look up the zombie with id 15, you would call it as if it were a function: zombies(15).
// query contract for info
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
// Call the function and do something with the result:
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});
Getting the user's account in MetaMask
We can see which account is currently active on the injected web3 variable via:
var userAccount = web3.eth.accounts[0]
setInterval
function to check if active user has changed:
var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call some function to update the UI with the new account
updateInterface();
}
}, 100);
send
functions
There are a few major differences from call functions:
1. sending a transaction requires a from address of who's calling the function (which becomes msg.sender in your Solidity code). We'll want this to be the user of our DApp, so MetaMask will pop up to prompt them to sign the transaction.
2. sending a transaction costs gas
3. There will be a significant delay from when the user sends a transaction and when that transaction actually takes effect on the blockchain. This is because we have to wait for the transaction to be included in a block, and the block time for Ethereum is on average 15 seconds. If there are a lot of pending transactions on Ethereum or if the user sends too low of a gas price, our transaction may have to wait several blocks to get included, and this could take minutes.
Thus we'll need logic in our app to handle the asynchronous nature of this code.
receipt
will fire when the transaction is included into a block on Ethereumerror
will fire if there's an issue that prevented the transaction from being included in a block, such as the user not sending enough gas.
payable
functions
Convert Ether to Wei:
// This will convert 1 ETH to Wei
web3js.utils.toWei("1");
Recall that levelup()
required 0.001 Ether. This is how it would be in Javascript(uses wei).
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001", "ether") })
Listening for New Zombies
Recall:event NewZombie(uint zombieId, string name, uint dna);
cryptoZombies.events.NewZombie()
.on("data", function(event) {
let zombie = event.returnValues;
// We can access this event's 3 return values on the `event.returnValues` object:
console.log("A new zombie was born!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);
But this code will fire an alert when any zombie gets created. What if we want alert only on our zombies?
Using indexed
Adding indexed
keyword to argument lets you filter events in front end.
Recall: event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
// Use `filter` to only fire this code when `_to` equals `userAccount`
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
// The current user just received a zombie!
// Do something here to update the UI to show it
}).on("error", console.error);
Querying past events
Use getPastEvents
query past events. fromBlock
and toBlock
is the range of events to see.
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
// `events` is an array of `event` objects that we can iterate, like we did above
// This code will get us a list of every zombie that was ever created
});
Because you can use this method to query the event logs since the beginning of time, this presents an interesting use case: Using events as a cheaper form of storage.
Finished index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CryptoZombies front-end</title>
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<div id="txStatus"></div>
<div id="zombies"></div>
<script>
var cryptoZombies;
var userAccount;
function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call a function to update the UI with the new account
getZombiesByOwner(userAccount)
.then(displayZombies);
}
}, 100);
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on("error", console.error);
}
function displayZombies(ids) {
$("#zombies").empty();
for (id of ids) {
// Look up zombie details from our contract. Returns a `zombie` object
getZombieDetails(id)
.then(function(zombie) {
// Using ES6's "template literals" to inject variables into the HTML.
// Append each one to our #zombies div
$("#zombies").append(`<div class="zombie">
<ul>
<li>Name: ${zombie.name}</li>
<li>DNA: ${zombie.dna}</li>
<li>Level: ${zombie.level}</li>
<li>Wins: ${zombie.winCount}</li>
<li>Losses: ${zombie.lossCount}</li>
<li>Ready Time: ${zombie.readyTime}</li>
</ul>
</div>`);
});
}
}
function createRandomZombie(name) {
// This is going to take a while, so update the UI to let the user know
// the transaction has been sent
$("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
// Send the tx to our contract:
return cryptoZombies.methods.createRandomZombie(name)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Successfully created " + name + "!");
// Transaction was accepted into the blockchain, let's redraw the UI
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
// Do something to alert the user their transaction has failed
$("#txStatus").text(error);
});
}
function feedOnKitty(zombieId, kittyId) {
$("#txStatus").text("Eating a kitty. This may take a while...");
return cryptoZombies.methods.feedOnKitty(zombieId, kittyId)
.send({ from: userAccount })
.on("receipt", function(receipt) {
$("#txStatus").text("Ate a kitty and spawned a new Zombie!");
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}
function levelUp(zombieId) {
$("#txStatus").text("Leveling up your zombie...");
return cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3.utils.toWei("0.001", "ether") })
.on("receipt", function(receipt) {
$("#txStatus").text("Power overwhelming! Zombie successfully leveled up");
})
.on("error", function(error) {
$("#txStatus").text(error);
});
}
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call()
}
function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call()
}
window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have Metamask installed
// Probably show them a message prompting them to install Metamask
}
// Now you can start your app & access web3 freely:
startApp()
})
</script>
</body>
</html>