CryptoKitties Clone Part 3 Adding Frontend
In last part we added an auction contract. Now we can buy and sell our GradientTokens. Let’s add some nice UI for that.
This is what we’ll be building:
Create React App
Let’s start by bootstrapping the front end with create-react-app
. From your projects root call:
create-react-app front
cd front
Create react app scripts won’t allow importing from outside the src
folder. Create a symlink to contract artifacts. o
ln -s ../../build/contracts src/contracts
I’m going to use truffle-contract
as an abstraction over web3
and mobx
to manage all the business logic. Install the dependencies:
yarn add mobx mobx-react truffle-contract
Update Migrations
First, we need to get the deployed contracts addresses. Let’s modify our migration so it will save the addresses to a JSON file.
First, we’ll deploy GradientToken
and then pass it’s address to TokenAuciton
on deploy. After both contracts are deployed – we get their addresses and save to a JSON file in front/src
folder.
Make your migrations/2_deploy_contract.js
look like this:
const GradientToken = artifacts.require("GradientToken");
const TokenAuction = artifacts.require("TokenAuction");
const util = require("util");
const fs = require("fs");
const path = require("path");
const writeFile = util.promisify(fs.writeFile);
module.exports = async function(deployer) {
const gradientToken = await deployer.deploy(GradientToken);
const auctionContract = await deployer.deploy(
TokenAuction,
GradientToken.address
);
const addresses = {
tokenAddress: GradientToken.address,
auctionAddress: TokenAuction.address
};
await writeFile(
path.join(__dirname, "..", "front", "src", "addresses.json"),
JSON.stringify(addresses)
);
};
Try to run the migrations. If you had them run before - run with --reset
option.
truffle migrate --reset
Check the front/src
folder. It should have addresses.json
.
Connect To Ethereum Network
Now we need to initialize the truffle-contract
. It needs the provider
that it will use to connect to ethereum network and contract addresses associated with deployed contracts.
Create the front/src/utils
folder and create getProvider.js
there with following contents:
import Web3 from "web3";
const getProvider = () => {
return new Web3.providers.HttpProvider("https://localhost:7545");
};
export default getProvider;
Here we connect directly to our local network. You can also use the provider injected to window
object by MetaMask. We’ll just use HttpProvider
for simplicity.
Let’s use our getProvider
function. Create front/src/utils/getGradientContractInstance
with following content:
import contract from "truffle-contract";
import getProvider from "utils/getProvider";
import GradientTokenArtifact from "contracts/GradientToken.json";
import addresses from "../addresses.json";
const { tokenAddress } = addresses;
export default async function getGradientContractInstance() {
const gradientTokenContract = contract(GradientTokenArtifact);
gradientTokenContract.setProvider(getProvider());
const gradientTokenInstance = await gradientTokenContract.at(tokenAddress);
return gradientTokenInstance;
}
Here we initialize truffle-contract
with JSON artifact, set provider and connect our contract to the deployed instance using the address from addresses.json
.
Now we have a utility function to get the contract instance – let’s use the MobX store to store it.
Create front/src/stores/ContractsStore.js
with following contents:
import { observable, decorate, action } from "mobx";
import getGradientContractInstance from "utils/getGradientContractInstance";
class ContractsStore {
gradientTokenInstance = null;
async setup() {
this.setGradientTokenInstance(await getGradientContractInstance());
}
setGradientTokenInstance(gradientTokenInstance) {
this.gradientTokenInstance = gradientTokenInstance;
}
}
export default decorate(ContractsStore, {
gradientTokenInstance: observable,
setGradientTokenInstance: action
});
The only function of this store is to have an observable reference to gradientTokenInstance
. Initially it null
but after setup
method is called it gets the instance using our getGradientContractInstance
utility function.
gradientTokenInstance
is observable so we can derive changes from it in other classes. Let’s create another MobX store.
Using The Contract
Create file front/src/stores/GradientTokenStore
. This token will be responsible for accessing contract methods.
import { observable, action, decorate, computed, when } from "mobx";
import randomColor from "utils/randomColor";
class GradientTokenStore {
tokens = [];
owner = null;
isLoading = true;
constructor(contractsStore) {
this.contractsStore = contractsStore;
when(() => this.gradientTokenInstance, this.setup);
}
get gradientTokenInstance() {
return this.contractsStore && this.contractsStore.gradientTokenInstance;
}
setup = async () => {
}
}
export default decorate(GradientTokenStore, {
owner: observable,
tokens: observable,
isLoading: observable,
gradientTokenInstance: computed
});
Here we defined the gradientTokenInstance
getter and made it computed
. MobX allows to observe observables
automatically and create computed
values based on them. You can read more about it in my MobX article
We observe this property in constructor
, using when
, so once gradientTokenInstance
is setup – we call the setup
method.
Getting Owner Address
To simplify the process – we’ll only use the address of the contract owner. Update the setup
function:
setup = async () => {
const owner = await this.gradientTokenInstance.owner();
this.setOwner(owner);
}
Create the setOwner
method we’ve just used, add the owner
class property with the initial value of null
and add it to the decorate
wrapper:
class GradientTokenStore {
tokens = [];
isLoading = true;
tokenIndex = 0;
owner = null;
// ... Other content
setOwner(owner) {
this.owner = owner;
}
}
export default decorate(GradientTokenStore, {
owner: observable,
// ... Other content
});
Fetching Tokens List
Add new method fetchTokens
to GradientTokenStore
:
fetchTokens = async () => {
const tokens = await this.gradientTokenInstance.tokensOf(this.owner);
const gradients = await Promise.all(
tokens.map(async token => {
return this.gradientTokenInstance.getGradient(token);
})
);
this.setIsLoading(false);
if (!gradients.length) {
return;
}
this.setTokens(this.indexedTokens(gradients));
};
First, we call our contract method tokensOf
. It will return an array of token ids, now we need to get associated gradients. We call the getGradient
method for every id. And in order to wait until all the promises will be resolved - we wrap it into Promise.all
After gradients are loaded - we update the isLoading
attribute, and if gradients
array is not empty we call the setTokens
method. setTokens
is trivial it is just a MobX action that sets the tokens
attribute:
setTokens(tokens) {
this.tokens.replace(tokens);
}
The indexedTokens
just adds an index to every gradient – it’s needed for React
. We are going to show our tokens in a list. And list elements in react should have unique key
prop:
indexedTokens(gradients) {
return gradients.map(gradient => {
return {
gradient,
index: this.tokenIndex++
};
});
}
Minting New Tokens
Now we can show the list of tokens but we can’t create new ones. Let’s fix it, add the mintToken
method to our GradientsTokenStore
:
mintToken = async () => {
const gradient = ['#fff', '#000'];
await this.gradientTokenInstance.mint(gradient[0], gradient[1], {
from: this.owner,
gas: 170000
});
this.appendToken({ gradient, index: this.tokenIndex++ });
};
Here I hard-coded the black and white gradient for simplicity.
Important note, here we call the transaction using httpProvider
. Transactions require gas. As we don’t use any kind of wallet here that would automatically calculate the required amount of gas – we set it manually.
After the mint
method was executed – we append the token to our list of tokens and provide an index to use in react list.
Add the appendToken
method:
appendToken(token) {
this.tokens.push(token);
}
Initializing Stores
Great, we are almost done with our business logic. Now we need to instantiate our stores. Create file front/src/stores/index.js
with following contents:
import ContractsStore from "./ContractsStore";
import GradientTokenStore from "./GradientTokenStore";
const contractsStore = new ContractsStore();
contractsStore.setup();
const gradientTokenStore = new GradientTokenStore(contractsStore);
export default {
contractsStore,
gradientTokenStore
};
Now modify your src/App.js
to inject our stores into the app.
import React, { Component } from "react";
import { Provider } from "mobx-react";
import TokensPage from "./TokensPage";
import stores from "./stores";
import "./App.css";
class App extends Component {
render() {
return (
<Provider {...stores}>
<div className="App">
<TokensPage />
</div>
</Provider>
);
}
}
export default App;
Now our stores will be available from any of the react views.
Making the views is trivial if you are interested – you can check out the repo