My file structure includes an etherium directory, that handles everything related to deploying and accessing my contract. A pages directory, with a nested games directory that maps directly to display urls - for instance pages/game/new maps to localhost/game/new. A components directory for components that are imported into the display pages. A test folder for contract and view tests (didn't write any view tests).
const routes = require('next-routes')();
routes
.add('/game/new', 'game/new')
.add('/game/:address', '/game/show')
module.exports = routes;
My routes directory remained rather simple. The most interesting piece here is the dynamic route for game/:address that renders the show component with the parameters sent from the url, which corresponds to the address of a rock paper scissors contract.
import React from 'react';
import Header from './Header';
import { Container } from 'semantic-ui-react';
import Head from 'next/head';
export default props => {
return(
<Container>
<Head>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"
/>
</Head>
<Header />
{props.children}
</Container>
)
}
Every display component is wrapped in my layout component. The layout includes a Container div that wraps everything to give the page some margin, the stylesheet for the semantic-ui library I've been using, and a header component that displays my navbar stuff at the top of every page. For instance, the render method on my index component now looks like this:
render() {
return(
<Layout>
<div>
<h3>Open Games</h3>
<Link route='/game/new'>
<a>
<Button
content = "Create game"
icon = "add circle"
primary
/>
</a>
</Link>
{this.renderGames()}
</div>
</Layout>
)
}
}
Everything is wrapped in the Layout component.
onSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true, errorMessage: '' })
try{
const accounts = await web3.eth.getAccounts()
await factory.methods.createRockPaperScissors(this.state.bet, this.state.name, this.state.bestOf)
.send({
from: accounts[0],
value: this.state.bet
})
Router.pushRoute('/')
} catch (err) {
this.setState({
errorMessage: err.message
})
}
this.setState({ loading: false })
}
My new game component is essentially a form that allows a user to deploy a new instance of a rock paper scissors contract. It includes form fields for game name, size of the bet, and how many games will be played (bestOf). The form itself isn't that interesting, but above is the function for submitting a rock paper scissors contract. It gets the users account from their metamask extension that they have to be using. It then calls the createRockPaperScissors method from the rcokpaperscissors factory contract that we have previously deployed. It passes the information from the form, using the .send method it transfers the value of the bet along with a gas fee to the contract. If it's successful the page returns to the homepage, where you should see a new contract populate.
One interesting thing happening on the game show page is just getting access to the game itself. There's a game.js file that looks like this:
import web3 from './web3'
import Game from './build/RockPaperScissors.json'
export default (address) => {
return new web3.eth.Contract(
JSON.parse(Game.interface),
address
)
}
It takes an address, and using the web3.js library, returns an instance of the game contract deployed to that address. We import this at the top of the component, and call it in the getInitialProps method, which get's called serverside before the component loads:
static async getInitialProps(props){
const game = Game(props.query.address)
const summary = await game.methods.game().call();
return {
address: props.query.address,
title: summary[0],
wager: summary[1],
playerOne: summary[2],
playerTwo: summary[3],
games: summary[4],
playerOneMove: summary[5],
playerTwoMove: summary[6],
playerOneWinCount: summary[7],
playerTwoWinCount: summary[8],
winner: summary[9],
result: summary[10],
completed: summary[11]
}
}
We then return a summary object that contains all the information recorded in our game hash.
I had some trouble getting access to the player. I tried a few things to get access to the account that was accessing the game page. The issue I was running into was that I couldn't call web3.eth.getAccounts(); in getInitialProps because that was happening server-side and didn't have access to the user's browser extension. I had to use the react lifecycle method componentDidMount, which gets called AFTER the first load of the page:
async componentDidMount() {
const players = await web3.eth.getAccounts();
this.setState({player: players[0]})
}
The rest of the page is just displaying the relevant information as players select moves. One interesting piece of code is the destructuring here:
renderInfo(){
const {
playerOneMove,
playerTwoMove,
playerOneWinCount,
playerTwoWinCount,
winner,
result,
completed,
} = this.props
This pulls all the information returned from getinitialprops() and sets them equal to a variable so i don't have to keep calling this.props.playerOneMove.
Selecting a move was a bit challenging. I had to pass the address of the player making the selection along with the address of player one and player two to call the proper method, either selectPlayerOneMove or selectPlayerTwoMove.
onSubmit = async (event) => {
event.preventDefault();
const game = Game(this.props.address)
this.setState({ loading: true, errorMessage: '' })
if(this.props.player == this.props.playerOne){
try{
await game.methods.playerOneMove(this.state.selected)
.send({
from: this.props.player
})
Router.pushRoute(`/game/${this.props.address}`)
} catch (err) {
this.setState({
errorMessage: err.message
})
}
this.setState({ loading: false })
} else {
try{
await game.methods.playerTwoMove(this.state.selected)
.send({
from: this.props.player
})
Router.pushRoute(`/game/${this.props.address}`)
} catch (err) {
this.setState({
errorMessage: err.message
})
}
this.setState({ loading: false })
}
}
I had to edit the contract to add in a piece of information to keep track of the results AFTER a game was completed, so both players didn't see it when another player selected a move. I also had an error where the contract was not receiving the ether from the player who created the game. It turns out I needed to create an anonymous function with no content that was marked "payable"... that took some googling.
function () public payable {
}