Building a fully decentralized User profile app on Ethereum and IPFS
Using Solidity, MetaMask, Truffle, Web3, Ganache, Webpack, and Bootstrap
The project
Welcome! This is a tutorial on building a distributed application on Ethereum and IPFS. The app enables users to sign up with a username, a title, and a short intro. It stores some of these fields on the Ethereum blockchain, and some off-chain on IPFS. It also lets everyone browse through the existing user profiles.
Here’s the full source code from github. I recommend you clone the repo instead of copy/pasting each code snippet, but if you prefer going slowly you can also just follow the steps below without cloning the repo.
Truffle — Setting up the project
A development framework for the Ethereum blockchain, Truffle helps us build, deploy, and interact with smart contracts through Web3, the Ethereum JavaScript API. Let’s install it (requires NodeJs 5+):
npm install -g truffle
Now lets make a folder for the project, initialize it with the webpack truffle box, and install dependencies through the npm package manager:
mkdir truffle-webpack-ipfs-bootstrap
truffle unbox webpack
npm install
The webpack truffle box comes with some great code examples, you should check them out if you’re new to the tools. For our purposes, we’ll clean them up to make room for our own contract:
rm contracts/MetaCoin.sol contracts/ConvertLib.sol
rm migrations/2_deploy_contracts.js
rm test/metacoin.js test/TestMetacoin.sol
Also remove the last two lines from .eslintignore:
migrations/2_deploy_contracts.js
test/metacoin.js
Now open up app/javascripts/app.js and remove or comment out the setCoin, refreshBalance, and setStatus functions, any calls to those functions, and references to the MetaCoin contract we deleted above. Also remove the web3 fallback code towards the end of the file (we’ll cover web3 below). You should end up with something like this:
// Import the page's CSS. Webpack will know what to do with it.
import "../stylesheets/app.css";// Import libraries we need.
import { default as Web3 } from 'web3';
import { default as contract } from 'truffle-contract'var accounts;
var account;window.App = {
start: function() {
var self = this;web3.eth.getAccounts(function(err, accs) {
if (err != null) {
alert("There was an error fetching your accounts.");
return;
}if (accs.length == 0) {
alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly.");
return;
}accounts = accs;
account = accounts[0];});
},};window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
console.warn("Using web3 detected from external source.");
// Use Mist/MetaMask's provider
window.web3 = new Web3(web3.currentProvider);
} else {
console.warn("No web3 detected. Please use MetaMask or Mist browser.");
}App.start();
});
Bootstrap 4 & jQuery— Adding UI frameworks
Next, let’s add the Boostrap 4 and jQuery to the project to help us build a responsive UI. We’ll go fast through this step (more details here).
To install Boostrap 4 and its dependencies, run the following commands:
npm install --save bootstrap@4
npm install --save jquery popper.js
npm install --save postcss-loader sass-loader
npm install --save node-sass precss
Then add the following rules to your webpack.config.js file:
{
test: /\.(scss)$/,
use: [{
loader: 'style-loader', // inject CSS to page
}, {
loader: 'css-loader', // translates CSS into CommonJS modules
}, {
loader: 'postcss-loader', // Run post css actions
options: {
plugins: function () { // post css plugins, can be exported to postcss.config.js
return [
require('precss'),
require('autoprefixer')
];
}
}
}, {
loader: 'sass-loader' // compiles Sass to CSS
}]
}
Now add the following lines to the top of your app/javascripts/app.js file and change app.css to app.scss:
// Import jquery
import jQuery from ‘jquery’;
window.$ = window.jQuery = jQuery;// Import bootstrap
import ‘bootstrap’;
// Import the scss for full app (webpack will package it)
import "../stylesheets/app.scss";
Next, rename you app/stylesheets/app.css to app.scss and paste the following directives and styles in it:
@import "custom";
@import "~bootstrap/scss/bootstrap";/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}body {
/* Margin bottom by footer height */
margin-bottom: 60px;
}.footer {
position: absolute;
bottom: 0;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;
line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
}/* Custom page CSS
-------------------------------------------------- */body > .container {
padding: 60px 15px 0;
}.footer > .container {
padding-right: 15px;
padding-left: 15px;
}.eth-address, {
color: #d0d0d0;
font-size: 0.6em;
}.card-subtitle {
color: #d0d0d0;
}
Now create a new file named app/stylesheets/_custom.scss and paste the following (this file lets you configure Bootstrap sass variables):
// Bootstrap variable overrides$enable-gradients: true;
Finally, open app/index.html and replace its contents with this simple page:
<!doctype html>
<html lang=”en”>
<head>
<! — Required meta tags →
<meta charset=”utf-8">
<meta name=”viewport” content=”width=device-width, initial-scale=1, shrink-to-fit=no”>
<script src=”./app.js”></script>
<title>Profiles</title>
</head>
<body><nav class=”navbar navbar-expand-lg navbar-primary bg-light”>
<button class=”navbar-toggler” type=”button” data-toggle=”collapse” data-target=”#navbarToggler” aria-controls=”navbarToggler” aria-expanded=”false” aria-label=”Toggle navigation”>
<span class=”navbar-toggler-icon”></span>
</button>
<a class=”navbar-brand” href=”#”>Profiles</a><div class=”collapse navbar-collapse” id=”navbarToggler”>
<ul class=”navbar-nav mr-auto mt-2 mt-lg-0">
<li class=”nav-item active”>
<a class=”nav-link” href=”/”>Home</a>
</li>
</div>
</nav><div class=”container-fluid”><div class=”alert alert-warning mt-3" role=”alert”>
This prototype is meant for demonstrations purposes only. Use at your own risk. When you create a profile, your information is publicly stored on the Ethereum blockchain, stored on IPFS, and displayed on this website.
</div><div class=”row”><div class=”col-lg-3 mt-1 mb-1">
<div class=”card card-profile-signup p-1"><form><div class=”card-body”>
<h5 class=”card-title”>Create your profile</h5><div class=”form-group”>
<label for=”username”>Username</label>
<input type=”text” class=”form-control” id=”sign-up-username” required=”required”>
</div><div class=”form-group”>
<label for=”username”>Title</label>
<input type=”text” class=”form-control” id=”sign-up-title”>
</div>
<div class=”form-group”>
<label for=”username”>Short intro</label>
<textarea class=”form-control” id=”sign-up-intro” rows=”2"></textarea>
</div><p>
ETH Address:
<span class=”eth-address”></span>
<input type=”text” class=”form-control” id=”sign-up-eth-address” value=”0x…” disabled>
</p>
</p><button type=”submit” class=”btn btn-primary” id=”sign-up-button”>Sign Up</button>
</div></form></div>
</div><div class=”col-lg-9 mt-1 mb-1" id=”users-div”>
</div></div></div><footer class=”footer”>
<div class=”container”>
<span class=”text-muted”>A demo dapp.</span>
</div>
</footer></body>
</html>
Congratulations! Your app stack and skeleton is now ready. Let’s have a look at it on the browser to make sure it’s working properly. To do so, start your development server with the following command:
npm run dev
You should look for two important outputs from the command above:
- “Project is running at http://localhost:8080/” (note your port number may differ if you already have a daemon running on 8080).
- “webpack: Compiled successfully”.
If any of those are missing, check your steps above. Otherwise, go ahead and open up Chrome and point it to http://localhost:8080/. You should see something like the screenshot on Figure 1 below.
Now open the Chrome JavaScript console. You should see a message like the one on Figure 2 below.
Let’s fix this!
Web3 and MetaMask — Interacting with the Ethereum blockchain
Web3 is the Ethereum JavaScript API. It lets you interact with the blockchain using JavaScript by enabling you to connect to an Ethereum node or provider and executing the methods they provide. You can setup different providers, but we’ll use MetaMask. I’ve found Web3 to be somewhat buggy and difficult to use, but it’s still fairly handy so we’ll go with it. The version npm installed when writing this post is 0.20.6. There’re multiple releases for v1 - a nice improvement - but we won’t use v1 as it’s still in beta.
MetaMask is a Chrome browser extension that implements a provider and hence makes it easy for users and Dapps to interact with any Ethereum network. Go ahead and install the extension, go through the privacy notes and terms of use, create your password, and save the auto-generated 12 words to a safe place. When successful, you should see something like the screenshot from Figure 3 below.
As you can see, MetaMask will create an account for you and connect to the main Ethereum network. For development purposes, we’ll connect to a local network instead. The next section covers how to set this up.
Ganache — Setting up your local Ethereum development network
Ganache provides us with an easy to install and use application that setups a local Ethereum network that we can use for development purposes. Go ahead and install Ganache. Note you can also use ‘truffle develop’ for this, but I personally prefer developing using Ganache (you can read more about these two different approaches here). BTW — you could also setup your own network using Go Ethereum (geth), but that’s a bit more involved and beyond the scope of this tutorial.
Tip: I don’t recommend developing using the ropsten test network — it’s too slow for development purposes. I think it’s great for staging purposes though!
Once installed, go ahead and launch Ganache. You should see something like Figure 4 below.
As you can see, Ganache has already started your network on http://127.0.0.1:7545 and created 5 accounts with 100 ETH each.
Now, let’s connect MetaMask to Ganache. To do so, click on the Main Network dropdown and select “Custom RPC”. Then enter “http://localhost:7545” under the “New RPC URL” input box, and hit Save. The dropdown menu should change to “Custom Network” as in Figure 5 below.
If you refresh your browser and look at http://localhost:8080/ again, you should no longer see the error on the JavaScript console and see a different message instead, as per Figure 6 below.
Tip: when developing using MetaMask, you’ll sometimes get errors regarding noonce being out of sync. This sometimes happens when you redeploy a contract or restart Ganache. You should be able to just reconnect to the network to sync MetaMask to the new network state, but MetaMask seems to still cache information, at least for me. As a workaround, you can remove the MetaMask extension and install it back again when this happens. See this thread for more.
Solidity — writing your Ethereum Smart Contract
Solidity is the de facto programming language for developing Ethereum Smart Contracts. It’s strongly typed (a good thing, given the nature of the business), and arguably hackish and difficult to use. We’ll still use it here for our purposes — it does the job.
Tip: this article is very helpful in understanding how to work with Solidity and how to develop basic CRUD functionality.
We’ll create a User contract. To do so, create a app/contracts/User.sol file with the following contents:
pragma solidity ^0.4.19;contract User {
mapping(address => uint) private addressToIndex;
mapping(bytes16 => uint) private usernameToIndex; address[] private addresses;
bytes16[] private usernames;
bytes[] private ipfsHashes;function User() public { // mappings are virtually initialized to zero values so we need to "waste" the first element of the arrays
// instead of wasting it we use it to create a user for the contract itself
addresses.push(msg.sender);
usernames.push('self');
ipfsHashes.push('not-available');}function hasUser(address userAddress) public view returns(bool hasIndeed)
{
return (addressToIndex[userAddress] > 0 || userAddress == addresses[0]);
}function usernameTaken(bytes16 username) public view returns(bool takenIndeed)
{
return (usernameToIndex[username] > 0 || username == 'self');
}
function createUser(bytes16 username, bytes ipfsHash) public returns(bool success)
{
require(!hasUser(msg.sender));
require(!usernameTaken(username));addresses.push(msg.sender);
usernames.push(username);
ipfsHashes.push(ipfsHash);addressToIndex[msg.sender] = addresses.length - 1;
usernameToIndex[username] = addresses.length - 1;
return true;
}function updateUser(bytes ipfsHash) public returns(bool success)
{
require(hasUser(msg.sender));
ipfsHashes[addressToIndex[msg.sender]] = ipfsHash;
return true;
}
function getUserCount() public view returns(uint count)
{
return addresses.length;
}// get by index
function getUserByIndex(uint index) public view returns(address userAddress, bytes16 username, bytes ipfsHash) {
require(index < addresses.length);return(addresses[index], usernames[index], ipfsHashes[index]);
}function getAddressByIndex(uint index) public view returns(address userAddress)
{
require(index < addresses.length);return addresses[index];
}function getUsernameByIndex(uint index) public view returns(bytes16 username)
{
require(index < addresses.length);return usernames[index];
}function getIpfsHashByIndex(uint index) public view returns(bytes ipfsHash)
{
require(index < addresses.length);return ipfsHashes[index];
}// get by address
function getUserByAddress(address userAddress) public view returns(uint index, bytes16 username, bytes ipfsHash) {
require(index < addresses.length);return(addressToIndex[userAddress], usernames[addressToIndex[userAddress]], ipfsHashes[addressToIndex[userAddress]]);
}function getIndexByAddress(address userAddress) public view returns(uint index)
{
require(hasUser(userAddress));return addressToIndex[userAddress];
}function getUsernameByAddress(address userAddress) public view returns(bytes16 username)
{
require(hasUser(userAddress));return usernames[addressToIndex[userAddress]];
}function getIpfsHashByAddress(address userAddress) public view returns(bytes ipfsHash)
{
require(hasUser(userAddress));return ipfsHashes[addressToIndex[userAddress]];
}// get by username
function getUserByUsername(bytes16 username) public view returns(uint index, address userAddress, bytes ipfsHash) {
require(usernameToIndex[username] < addresses.length);return(usernameToIndex[username], addresses[usernameToIndex[username]], ipfsHashes[usernameToIndex[username]]);
}function getIndexByUsername(bytes16 username) public view returns(uint index)
{
require(usernameTaken(username));return usernameToIndex[username];
}function getAddressByUsername(bytes16 username) public view returns(address userAddress)
{
require(usernameTaken(username));return addresses[usernameToIndex[username]];
}function getIpfsHashByUsername(bytes16 username) public view returns(bytes ipfsHash)
{
require(usernameTaken(username));return ipfsHashes[usernameToIndex[username]];
}}
Think about your Smart Contract as an API your application exposes to the blockchain world. We’ll call this API from our own Dapp, but other contracts and wallets can call it too (so they can build on top of it). Here’s a quick description of what our User contract does:
- It exposes an API for any application to create a User.
- For every User, it stores their chosen username, their Ethereum address, and their IPFS hash (more on IPFS below). You can easily expand this pattern to add other fields or external IDs of pretty much any sort (at the expense of higher gas cost).
- It guarantees username uniqueness and address uniqueness (a given Ethereum address can only have one user, and once a username is taken it cannot be used by anyone else).
- It exposes API methods for any application to get the username, address, and IPFS hash for all the users.
- It creates its own user (under username ‘self’) with the contract owner’s Ethereum address.
Tip: try out Remix for help learning Solidity and developing smart contracts. Develop and refine your initial contracts on Remix, then move over to your local network (you can also connect Remix to your local network BTW).
Truffle — compiling & deploying the Smart Contract
It’s now time to compile our smart contract and deploy it to the development network powered by Ganache. While you can do this manually, Truffle makes this easier. In order for Truffle to help us we need to define a migration for our new contract. To do so, create a file named migrations/2_deploy_user.js with the following content:
var User = artifacts.require(“User”);module.exports = function(deployer) {
deployer.deploy(User);
};
Now we can compile and deploy (migrate) the contracts to our development network (make sure Ganache is running):
truffle compile
truffle migrate
If that went well, you should see a few transactions on the Ganache UI as in Figure 7.
Tip: Ganache reinitializes the network on each start, so make sure you redeploy contracts when you restart Ganache.
Tip2: Truffle remembers where contracts have been deployed to (stored in build/contracts/*.json files). For quick redeployment, you can simply delete build/contracts/*.json files and then run compile & migrate commands again. Do not use this quick method if you‘ve deployed the contract to other networks though (I haven’t tested this, but I’d try editing the corresponding json file section instead in that case).
While our Dapp will interact with the contract through Web3 and MetaMask, we can also use the Truffle console to interact with it and manually test the API. This gives us a sandbox to play with that’s isolated from any web3-related issues.
Tip3: you can also run compile & migrate directly from the Truffle console.
Get users count:
truffle consoletruffle(development)> User.deployed().then(function(contractInstance) { contractInstance.getUserCount().then(function(v) {console.log(v)})})...truffle(development)> BigNumber { s: 1, e: 0, c: [ 1 ] }
Get User by Index:
truffle(development)> User.deployed().then(function(contractInstance) {contractInstance.getUserByIndex(0).then(function(v) {console.log(v)})})...truffle(development)> [ ‘0x176f72999d3f7e88abe913aee381443ffbcd7caa’,‘0x73656c66000000000000000000000000’,‘0x6e6f742d617661696c61626c65’ ]
We now have a working User contract on our development network!
Web3 & MetaMask — interacting with the Smart Contract
Now that we have a working User contract, we just need to glue our application UI to interact with it using Web3 & MetaMask.
Setup
First, let’s initialize a User class that we’ll use to call the contract with Web3. Open your app/javascripts/app.js file and add the following code (after importing contract from truffle-contract):
import user_artifacts from ‘../../build/contracts/User.json’
var User = contract(user_artifacts);
Then add the following to the start function inside (at the end of) the web3.eth.getAccounts call:
// set the provider for the User abstraction User.setProvider(web3.currentProvider);
Now that that’s ready, we can start calling the contract.
Create User
Let’s start by creating users. Add a createUser function to window.App:
createUser: function() {
var username = $(‘#sign-up-username’).val();
var title = $(‘#sign-up-title’).val();
var intro = $(‘#sign-up-intro’).val();
// var ipfsHash = ‘’;
var ipfsHash = ‘not-available’;console.log(‘creating user on eth for’, username, title, intro, ipfsHash);User.deployed().then(function(contractInstance) {
contractInstance.createUser(username, ipfsHash, {gas: 200000, from: web3.eth.accounts[0]}).then(function(success) {
if(success) {
console.log(‘created user on ethereum!’);
} else {
console.log(‘error creating user on ethereum’);
}
}).catch(function(e) {
// There was an error! Handle it.
console.log(‘error creating user:’, username, ‘:’, e);
});
});
Now show the current Ethereum address and trigger create user on form submit by adding these lines at the end of your getAccounts call:
// show current address
var ethAddressIput = $('#sign-up-eth-address').val(accounts[0]);// trigger create user when sign up is clicked
var signUpButton = $(‘#sign-up-button’).click(function() {
self.createUser();
return false;
});
Now go back to your browser and hit refresh. We’re now almost ready to create a user, however, we’ll need an account with an ETH balance in order to call the createUser method from our contract (it needs to pay for gas).
Open Ganache, go to the Accounts tab, and click on the key icon for any of the accounts, and copy the private key. Then go back to MetaMask, click on the accounts icon (the little person-like icon with a refresh type circle around it), choose “Import Account”, paste the private key, and hit “Import”. You should now have an Account 2 available (and selected) in MetaMask with the same balance shown in Ganache.
Go ahead and fill in a username, title, and short intro, then hit Sign Up. If all goes well, MetaMask should ask you to confirm and submit the transaction, as shown in Figure 8 below.
Go ahead and hit Submit. Your JavaScript console should eventually show the “created user on ethereum!” message. And, if you now call the getUserByIndex method with index = 1 from your truffle console, you should see the respective encoding for your address, username and a fixed IPFS hash string (more on IPFS below).
Getting Users
We’ll now add a way for our app to display all the users. Start by adding a getAUser method to your window.App:
getAUser: function(instance, i) {var instanceUsed = instance;
var username;
var ipfsHash;
var address;
var userCardId = ‘user-card-’ + i;return instanceUsed.getUsernameByIndex.call(i).then(function(_username) {console.log(‘username:’, username = web3.toAscii(_username), i);
$(‘#’ + userCardId).find(‘.card-title’).text(username);
return instanceUsed.getIpfsHashByIndex.call(i);}).then(function(_ipfsHash) {console.log(‘ipfsHash:’, ipfsHash = web3.toAscii(_ipfsHash), i);
if(ipfsHash != ‘not-available’) {
// …
}
return instanceUsed.getAddressByIndex.call(i);
}).then(function(_address) {console.log(‘address:’, address = _address, i);
$(‘#’ + userCardId).find(‘.card-eth-address’).text(address);
return true;}).catch(function(e) {console.log(‘error getting user #’, i, ‘:’, e);});},
Tip: calling the getUserByIndex contract method from web3 didn’t work for me with the current web3 version: web3 wasn’t able to handle returning a bytes16 and bytes variable at the same time. To workaround this, I call on each individual field instead, which brings me to …
Tip2: chaining together Truffle calls can be somewhat challenging. Here’s a helpful link.
Now that we have a method to fetch a given user from the blockchain, let’s write a method to iterate through all the users and populate them on the UI. Add the getUsers function below to your window.App:
// Fetch all users from the blockchain - eventually we'll probably need to paginate this
getUsers: function() {
var self = this;var instanceUsed;User.deployed().then(function(contractInstance) {instanceUsed = contractInstance;return instanceUsed.getUserCount.call();}).then(function(userCount) {userCount = userCount.toNumber();console.log('User count', userCount);var rowCount = 0;
var usersDiv = $('#users-div');
var currentRow;for(var i = 0; i < userCount; i++) {var userCardId = 'user-card-' + i;if(i % 4 == 0) {
var currentRowId = 'user-row-' + rowCount;
var userRowTemplate = '<div class="row" id="' + currentRowId + '"></div>';
usersDiv.append(userRowTemplate);
currentRow = $('#' + currentRowId);
rowCount++;
}var userTemplate = `
<div class="col-lg-3 mt-1 mb-1" id="` + userCardId + `">
<div class="card bg-gradient-primary text-white card-profile p-1">
<div class="card-body">
<h5 class="card-title"></h5>
<h6 class="card-subtitle mb-2"></h6>
<p class="card-text"></p>
<p class="eth-address m-0 p-0">
<span class="card-eth-address"></span>
</p>
</div>
</div>
</div>`;currentRow.append(userTemplate);}console.log("getting users...");for(var i = 0; i < userCount; i++) {self.getAUser(instanceUsed, i);}});},
Finally, lets call getUsers at the end of the eth.getAccounts:
// populate users
self.getUsers();
Now go back and refresh your browser, you should see something like the screenshot from Figure 9 below.
If you’ve made it this far, congratulations, you have a working Dapp that can interact with your own smart contract through MetaMask!
IPFS — decentralized off-chain information storage and retrieval
If you’ve followed along, you may’ve noticed that performing a createUser transaction costs gas. This is because we’re writing information to the blockchain (submitting a transaction). In contrast, read operations (calls, such as getUserNameByIndex) are free of charge. Read this for more on Truffle transactions vs. calls.
Unless your business model permits (or you intend to deploy to a private network), storing all your information on the blockchain may be cost prohibiting. In addition, when you run a transaction (write) on the live main network, the performance is not going to be that fast as it is with our development/local network — your transaction needs to be mined and this can take a little while. So, your performance/experience requirements may also not be compatible with the idea of storing everything on the blockchain. Plus, Solidity is not particularly easy to work with either.
Tip1: there are other ways to let users sign up that don’t cost gas. Read this article for more.
Tip2: we’re now to the point where architecture choices start becoming more evident. Read about the different alternatives on this great article.
For this tutorial, we want to keep our dapp fully decentralized, so we don’t want to use anything like S3, Firebase, or our own server infrastructure to process/store/retrieve information. Enter IPFS.
While not technically needed to run the app, I suggest you install IPFS to become acquainted with it:
brew install ipfs
If you don’t have homebrew or don’t want to use it, follow the installation instructions from the IPFS site instead.
First, you’ll have to initialize your local IPFS copy. Similarly to git, IPFS works with a local and global repository. You’ll have to init your local repo only once:
ipfs init
Next, let’s bring up the IPFS daemon:
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]"
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\"true\"]"
ipfs daemon
Tip1: The IPFS daemon runs on port 8080. If your dev server is already running on that port, stop it (Ctrl+C) before proceeding (it will use a different port when you restart it if 8080 is already taken).
Tip2: The above IPFS configuration is insecure. It’s a quick config to allow CORS but it doesn’t validate who’s calling your daemon. This is meant for development and experimentation purposes only. Please refer to the IPFS documentation for more details.
Now, let’s have our app talk with IPFS as well. We’ll store the user’s title and intro fields on IPFS, and store the IPFS hashes that point to this content on the blockchain. First, install the IPFS JavaScript client library package:
npm install --save ipfs-api
Then add the following lines to your app.js file (just before window.App):
const ipfsAPI = require(‘ipfs-api’);
const ipfs = ipfsAPI(‘localhost’, ‘5001’);
// const ipfs = ipfsAPI(‘ipfs.infura.io’, ‘5001’, {protocol: ‘https’});
That will initialize the client and define the IPFS daemon address. Now add the following lines in your start function:
ipfs.id(function(err, res) {
if (err) throw err
console.log(“Connected to IPFS node!”, res.id, res.agentVersion, res.protocolVersion);
});
Now go back to your browser and hit refresh (remember to restart your dev server if you stopped it in order to run the IPFS daemon, plus point your browser to a new port as necessary — most likely 8081). Your JavaScript console should show the “Connected to IPFS node!” message along with the hash for your daemon and some other version information.
Now that we’re connected, let’s update the createUser function to store the username, title, and intro in IPFS, get a hash, and store the hash on the blockchain. Here’s the updated createUser function code to do this:
createUser: function() {
var username = $(‘#sign-up-username’).val();
var title = $(‘#sign-up-title’).val();
var intro = $(‘#sign-up-intro’).val();
var ipfsHash = ‘’;console.log(‘creating user on ipfs for’, username);var userJson = {
username: username,
title: title,
intro: intro
};ipfs.add([Buffer.from(JSON.stringify(userJson))], function(err, res) {
if (err) throw err
ipfsHash = res[0].hashconsole.log(‘creating user on eth for’, username, title, intro, ipfsHash);User.deployed().then(function(contractInstance) {
// contractInstance.createUser(web3.fromAscii(username), web3.fromAscii(title), intro, ipfsHash, {gas: 2000000, from: web3.eth.accounts[0]}).then(function(index) {
contractInstance.createUser(username, ipfsHash, {gas: 200000, from: web3.eth.accounts[0]}).then(function(success) {
if(success) {
console.log(‘created user on ethereum!’);
} else {
console.log(‘error creating user on ethereum’);
} }).catch(function(e) {
// There was an error! Handle it.
console.log(‘error creating user:’, username, ‘:’, e);
});
});
});
}
Go back to your browser, and create a new user (note that, as per our Solidity contract, each Ethereum address can only have one user, so you’ll need to first import a different account to MetaMask, then refresh the browser and ensure the new address is reflected in the sign up form). Make sure to also use a different username (username uniqueness is also enforced by the contract). If all goes well, you should see the IPFS hash on your JavaScript console logs, and it should be written to the blockchain along along with the other user information.
Now we need to retrieve the (JSON-formatted) user fields we stored on IPFS. To do so, update your getAUser method with the following code:
getAUser: function(instance, i) {var instanceUsed = instance;
var username;
var ipfsHash;
var address;
var userCardId = 'user-card-' + i;return instanceUsed.getUsernameByIndex.call(i).then(function(_username) {console.log('username:', username = web3.toAscii(_username), i);
$('#' + userCardId).find('.card-title').text(username);
return instanceUsed.getIpfsHashByIndex.call(i);}).then(function(_ipfsHash) {console.log('ipfsHash:', ipfsHash = web3.toAscii(_ipfsHash), i);// $('#' + userCardId).find('.card-subtitle').text('title');if(ipfsHash != 'not-available') {
var url = 'https://ipfs.io/ipfs/' + ipfsHash;
console.log('getting user info from', url);$.getJSON(url, function(userJson) {console.log('got user info from ipfs', userJson);
$('#' + userCardId).find('.card-subtitle').text(userJson.title);
$('#' + userCardId).find('.card-text').text(userJson.intro);});
}return instanceUsed.getAddressByIndex.call(i);
}).then(function(_address) {console.log('address:', address = _address, i);
$('#' + userCardId).find('.card-eth-address').text(address);return true;}).catch(function(e) {// There was an error! Handle it.
console.log('error getting user #', i, ':', e);});}
Go back to your browser and hit refresh one last time: you should now see the last user’s title and intro coming from IPFS!
Now … remember we mentioned you do not need an IPFS daemon running to run the app? That’s right, you can use Infura’s public IPFS daemon instead. To do so, simply comment the localhost line and comment out the Infura one. Then refresh your browser and see your user info still showing up. Pretty cool, huh?
Tip: the IPFS network won’t keep your data alive forever unless you have multiple nodes requesting the hash often enough. There’s some help for this (like pinning your hashes, running your own IPFS daemon or cluster, leveraging the docker image, using a pinning service, etc.), but that’s beyond our scope. Also see this article.
Tip2: if you implement an update user function, make sure to update the IPFS hash on the blockchain record (you’ll get a new hash from IPFS when you change the content since IPFS is content-addressable). You could also use IPNS to implement a different mechanism but that would likely require generating different IPFS key pairs per user. See this reddit thread.
Tip3: You can also run, at least in theory, an IPFS node in the browser using JavaScript, but note the project is in early alpha.
IPFS — deploying the app
Now that you’ve become acquainted with IPFS, you may also want to deploy the actual app to the network. To do so, you’ll first have to build the project:
npm run build
This tells webpack to package and build your app. The output of this command should be two new files: build/app.js, and build/index.html.
Now we just need to push the build folder to IPFS. With your ipfs daemon running, issue the following commands:
cd build
ipfs add . -r
Now copy & paste the last hash from the output, open up your browser and go to https://ipfs.io/ipfs/<your hash>. Note that it may take a minute for your files to become available.
Note that the app will still only interact with your development network (you can still connect to it through MetaMask using the deployed app). If you want the app to work on other networks (e.g.: the ropsten test network, live main network, etc.), you’ll have to deploy the contracts to them (and redeploy to IPFS so that the new /build/contract/*.json files are available). You can read more on how to do that here.
Finally, you can also setup DNS to point to IPFS. It’s really easy, you just need to add an A record that points to the ipfs.io IP address, and a TXT record that links to the IPFS hash. You can read more about IPNS and how to use DNS records on this article.
Context and Motivation
As a tech entrepreneur and engineer, it’s vital to keep up with new technologies as its oftentimes the key to unleashing innovation and disruption while staying relevant career-wise. I believe there’s a unique technology innovation trifecta going on right now with Artificial Intelligence, Blockchain, and Internet of Things, and it’s just too exciting and too significant not to jump onto.
The technological advancements in these three areas combined is already changing how we interact with computers and with one another, and we’ve only seen the very beginning. Up until now, I knew AI, but didn’t speak blockchain. For the past few months, I’ve been catching up with the blockchain world (and specifically Ethereum). This tutorial summarizes and shares some of my learnings. I still don’t speak IoT.
Final comments
Please note this code is not production ready (there’s more error management needed, the UX clearly needs improvement, and the Solidity contract is missing Truffle tests and events, just to name a few). Feel free to use the code at your own risk. Also, note that some of the projects this project relies upon are in very early stages and aren’t production ready yet either. I’m also not an expert here, so there may be better ways to do what I’m doing.
While the app doesn’t do much by itself, building it takes us through the development lifecycle and forces us to find workarounds for some of the most common issues. I hope it’s a good starting point for building more interesting Dapps in the future. Thank you for reading!
Update: If you enjoyed this tutorial, checkout my post on “Pre-Launching CryptoArte”, the project leverages and uses a lot of the techniques covered here!