Creating a React Front-End
This section is dedicated to creating a simple graphical front-end for the
Millionaires Problem example
using React, a JavaScript library for building component-oriented user interfaces.
The enigma-template-dapp repository is
based off of the truffle-react box variant,
which allows for the rapid deployment of a unique front-end with the related packages
and logic separated from the code — in this case, located in the /client/src folder.
Please note that the below section is almost entirely generic React code that does not have any role in interfacing with the secret contracts made earlier. There are a couple of small, unique portions, noted by their comment annotations within the code sections below.
If you are unfamiliar with React, it is advised to read this quick tutorial to get a grasp of the basic concepts and components involved.
The full source code for the demo application that is built using this section can be found here.
1. Enter the /client/src folder of the enigma-template-dapp directory.
2. Open up App.js in your favorite text editor and paste the following code:
import React, { Component } from "react";
import getContractInstance from "./utils/getContractInstance";
import EnigmaSetup from "./utils/getEnigmaSetup";
import millionairesProblemFactoryContractDefinition from "./contracts/MillionairesProblemFactory.json";
import millionairesProblemContractDefinition from "./contracts/MillionairesProblem.json";
import { Container, Message } from "semantic-ui-react";
import Header from "./Header";
import MillionairesProblemWrapper from "./MillionairesProblemWrapper";
import Paper from "@material-ui/core/Paper";
import "./App.css";
const GAS = "1000000";
class App extends Component {
constructor(props) {
super(props);
this.state = {
enigmaSetup: null,
millionairesProblemFactory: null,
millionairesProblem: null
};
}
componentDidMount = async () => {
/*
Initialize bundled object containing web3, accounts, Enigma/EnigmaToken contracts, and
enigma/principal wrappers
*/
let enigmaSetup = new EnigmaSetup();
await enigmaSetup.init();
// Initialize MillionairesProblemFactory contract instance
const millionairesProblemFactory = await getContractInstance(
enigmaSetup.web3,
millionairesProblemFactoryContractDefinition
);
const millionairesProblems = await millionairesProblemFactory.getMillionairesProblems.call();
// If we've deployed at least one MillionaireProblem, get the latest one
if (millionairesProblems.length != 0) {
const millionairesProblem = await getContractInstance(
enigmaSetup.web3,
millionairesProblemContractDefinition,
millionairesProblems[millionairesProblems.length - 1]
);
this.setState({
millionairesProblem
});
}
this.setState({ enigmaSetup, millionairesProblemFactory });
};
// Create fresh, new MillionaireProblem contract
async createNewMillionairesProblem() {
await this.state.millionairesProblemFactory.createNewMillionairesProblem({
from: this.state.enigmaSetup.accounts[0],
gas: GAS
});
// Obtain the latest-deployed MillionaireProblem instance, this will be the one we interact with
const millionairesProblems = await this.state.millionairesProblemFactory.getMillionairesProblems.call();
const millionairesProblem = await getContractInstance(
this.state.enigmaSetup.web3,
millionairesProblemContractDefinition,
millionairesProblems[millionairesProblems.length - 1]
);
this.setState({
millionairesProblem
});
}
render() {
if (!this.state.enigmaSetup) {
return (
<div className="App">
<Header />
<Message color="red">Enigma setup still loading...</Message>
</div>
);
} else {
return (
<div className="App">
<Header />
<br />
<Container>
<Paper>
<MillionairesProblemWrapper
onCreateNewMillionaresProblem={() => {
this.createNewMillionairesProblem();
}}
enigmaSetup={this.state.enigmaSetup}
millionairesProblem={this.state.millionairesProblem}
/>
</Paper>
</Container>
</div>
);
}
}
}
export default App;
The file App.js is essentially the index page of your dApp’s interface, which will handle:
Initialization of the
EnigmaSetupobject [lines 28–29] mentioned above and the top-level rendering of the dApp.Pulling in the contract ABIs for our custom contracts [lines 4–5] and appropriately setting the state for the current
MillionairesProblemif it exists [lines 35–48].
3. In the same directory, create a file named MillionairesProblemWrapper.js and paste the following code:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import { Message } from "semantic-ui-react";
import AddMillionaireDialog from "./AddMillionaireDialog";
const engUtils = require("./lib/enigma-utils");
// Specify the signature for the callable and callback functions, make sure there are NO spaces
const CALLABLE = "computeRichest(address[],uint[])";
const CALLBACK = "setRichestAddress(address)";
const ENG_FEE = 1;
const GAS = "1000000";
const styles = theme => ({
button: {
display: "block",
marginTop: theme.spacing.unit * 2
}
});
class MillionairesProblemWrapper extends Component {
constructor(props) {
super(props);
this.state = {
numMillionaires: null,
richestAddress: "TBD"
};
this.handleSubmit = this.handleSubmit.bind(this);
this.addMillionaire = this.addMillionaire.bind(this);
}
componentDidMount = async () => {
/*
Check if we have an instance of the MillionairesProblem deployed or not before
we call any functions on it
*/
if (this.props.millionairesProblem != null) {
let numMillionaires = await this.props.millionairesProblem.numMillionaires.call();
numMillionaires = numMillionaires.toNumber();
this.setState({ numMillionaires });
}
};
// Handles re-rendering if we've created a new MillionairesProblem (callback resides in parent)
async componentWillReceiveProps(nextProps) {
if (this.props.millionairesProblem != nextProps.millionairesProblem) {
this.setState({ numMillionaires: 0, richestAddress: "TBD" });
}
}
/*
Callback for adding a new millionaire. Note that we are encrypting data
(address and net worth) in this function and pass in those values to the contract
*/
async addMillionaire(address, netWorth) {
let encryptedAddress = getEncryptedValue(address);
let encryptedNetWorth = getEncryptedValue(netWorth);
await this.props.millionairesProblem.addMillionaire(
encryptedAddress,
encryptedNetWorth,
{ from: this.props.enigmaSetup.accounts[0], gas: GAS }
);
let numMillionaires = await this.props.millionairesProblem.numMillionaires.call();
numMillionaires = numMillionaires.toNumber();
this.setState({ numMillionaires });
}
/*
Creates an Enigma task to be computed by the network.
*/
async enigmaTask() {
let numMillionaires = await this.props.millionairesProblem.numMillionaires.call();
let encryptedAddresses = [];
let encryptedNetWorths = [];
// Loop through each millionaire to construct a list of encrypted addresses and net worths
for (let i = 0; i < numMillionaires; i++) {
// Obtain the encrypted address and net worth for a particular millionaire
let encryptedValue = await this.props.millionairesProblem.getInfoForMillionaire.call(
i
);
encryptedAddresses.push(encryptedValue[0]);
encryptedNetWorths.push(encryptedValue[1]);
}
let blockNumber = await this.props.enigmaSetup.web3.eth.getBlockNumber();
/*
Take special note of the arguments passed in here (blockNumber, dappContractAddress,
callable, callableArgs, callback, fee, preprocessors). This is the critical step for how
you run the secure computation from your front-end!!!
*/
let task = await this.props.enigmaSetup.enigma.createTask(
blockNumber,
this.props.millionairesProblem.address,
CALLABLE,
[encryptedAddresses, encryptedNetWorths],
CALLBACK,
ENG_FEE,
[]
);
let resultFee = await task.approveFee({
from: this.props.enigmaSetup.accounts[0],
gas: GAS
});
let result = await task.compute({
from: this.props.enigmaSetup.accounts[0],
gas: GAS
});
console.log("got tx:", result.tx, "for task:", task.taskId, "");
console.log("mined on block:", result.receipt.blockNumber);
}
// onClick listener for Check Richest button, will call the enigmaTask from here
async handleSubmit(event) {
event.preventDefault();
let richestAddress = "Computing richest...";
this.setState({ richestAddress });
// Run the enigma task secure computation above
await this.enigmaTask();
// Watch for event and update state once callback is completed/event emitted
const callbackFinishedEvent = this.props.millionairesProblem.CallbackFinished();
callbackFinishedEvent.watch(async (error, result) => {
richestAddress = await this.props.millionairesProblem.richestMillionaire.call();
this.setState({ richestAddress });
});
}
render() {
const { classes } = this.props;
if (this.state.numMillionaires == null) {
return (
<div>
<Button onClick={this.props.onCreateNewMillionaresProblem}>
{"Create New Millionaires' Problem"}
</Button>
</div>
);
} else {
return (
<div>
<Button
onClick={this.props.onCreateNewMillionaresProblem}
variant="contained"
>
{"Create New Millionaires' Problem"}
</Button>
<h2>Num Millionaires = {this.state.numMillionaires}</h2>
<h2>Richest Millionaire = {this.state.richestAddress}</h2>
<AddMillionaireDialog
accounts={this.props.enigmaSetup.accounts}
onAddMillionaire={this.addMillionaire}
/>
<br />
<Button
onClick={this.handleSubmit}
disabled={this.state.numMillionaires == 0}
variant="contained"
color="secondary"
>
Check Richest
</Button>
</div>
);
}
}
}
// Function to encrypt values (in this case either address or net worth)
function getEncryptedValue(value) {
let clientPrivKey =
"853ee410aa4e7840ca8948b8a2f67e9a1c2f4988ff5f4ec7794edf57be421ae5";
let enclavePubKey =
"0061d93b5412c0c99c3c7867db13c4e13e51292bd52565d002ecf845bb0cfd8adfa5459173364ea8aff3fe24054cca88581f6c3c5e928097b9d4d47fce12ae47";
let derivedKey = engUtils.getDerivedKey(enclavePubKey, clientPrivKey);
let encrypted = engUtils.encryptMessage(derivedKey, value);
return encrypted;
}
MillionairesProblemWrapper.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(MillionairesProblemWrapper);
The MillionairesProblemWrapper.js component drives the business logic for the MillionairesProblem that’s being rendered and handles:
The logic to
addMillionaire(committing encrypted values for the address and net worth to the contract) [lines 51–66].Rendering the current state of the contract (number of participants, richest address) [lines 145–146].
The
enigmaTask(in charge of executing the secret computation, running the callable and callback functions with the encrypted arguments necessary) [lines 68–109].
4. In the same directory, create a file named AddMillionaireDialog.js and paste the following code:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
const styles = theme => ({
button: {
display: "block",
marginTop: theme.spacing.unit * 2
},
formControl: {
margin: theme.spacing.unit,
minWidth: 120
}
});
class AddMillionaireDialog extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
millionaireAddress: "None",
millionaireNetWorth: null
};
this.handleChangeAddress = this.handleChangeAddress.bind(this);
this.handleChangeNetWorth = this.handleChangeNetWorth.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleClickOpen = () => {
this.setState({ open: true });
};
handleClose = () => {
this.setState({ open: false });
};
// onChange listener to update state with user-input address
handleChangeAddress(event) {
this.setState({ millionaireAddress: event.target.value });
}
// onChange listener to update state with user-input net worth
handleChangeNetWorth(event) {
this.setState({ millionaireNetWorth: event.target.value });
}
// onClick listener to update to trigger addMillionaire callback from parent component
async handleSubmit(event) {
event.preventDefault();
// Trigger MillionairesProblemWrapper addMillionaire callback
this.props.onAddMillionaire(
this.state.millionaireAddress,
this.state.millionaireNetWorth
);
this.setState({
open: false,
millionaireAddress: "None",
millionaireNetWorth: null
});
}
render() {
const { classes } = this.props;
return (
<div>
<Button
onClick={this.handleClickOpen}
variant="contained"
color="primary"
>
Add Millionaire
</Button>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">Add Millionaire</DialogTitle>
<DialogContent>
<DialogContentText>
To add yourself, please set your address and state your net
worth...
</DialogContentText>
<form className={classes.root} onSubmit={this.handleSubmit}>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="millionaireAddress">
Millionaire Address
</InputLabel>
<Select
value={this.state.millionaireAddress}
onChange={this.handleChangeAddress}
inputProps={{
name: "address",
id: "millionaireAddress"
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{this.props.accounts.map((account, i) => {
return (
<MenuItem key={i} value={account}>
{account}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="millionaireNetWorth">
Millionaire Net Worth
</InputLabel>
<Input
id="millionaireNetWorth"
onChange={this.handleChangeNetWorth}
autoComplete="off"
/>
</FormControl>
</form>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Cancel
</Button>
<Button onClick={this.handleSubmit}>Add Millionaire</Button>
</DialogActions>
</Dialog>
</div>
);
}
}
AddMillionaireDialog.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(AddMillionaireDialog);
This AddMillionaireDialog.js file is a material-ui-driven dialog script which handles:
Inputting address and net worth [lines 48–56].
Triggering the
addMillionairecallback from the above file, which commits the encrypted address and net worth to the activeMillionaireProblemcontract [lines 58–71].
5. To build your changes, simply rerun npm run start in the /client folder. You should now see your new interface loaded.
Conclusion
That’s it! By now you have successfully deployed the Enigma Docker Test Network, created a template dApp, and built a graphical front-end using React. For more information on how Enigma functions, refer to the following few sections.
Have questions or issues? Stop by our developer forum and let us know!