There might be a situation where you want to be able to store user profiles on an Ethereum blockchain so that you can make sure to track every update that’s made. This might be to ensure that NFTs are always associated with the proper creator. It could also be to make sure that sensitive data is stored more securely than in a regular database.
That’s why we’ll be making a Dapp (distributed app) that’s hosted on an Ethereum network. We’ll get the most sensitive information from the blockchain and we’ll also have data in a regular database. All of this will be nicely bundled into a Redwood app.
Working with blockchain can seem a little tricky at first. That’s why we’ll get a few things in place first. If you don’t have Ganache installed, go ahead and download that here. This will give you access to a local Ethereum network you can use to deploy smart contracts.
You’ll also need a local Postgres instance since that’s the regular database we’ll work with. You can download that for free if you don’t already have it.
The last thing we need to set up is the project itself. Since we’ll be working with Redwood, we can generate the files and folders we need for both the front-end and back-end with the following command.
$ yarn create redwood-app --typescript user-profile-dapp
This bootstraps a fully functional full-stack app that we can modify to fit our Dapp. The two main directories we’ll be working with are api
and web
which contain the code and folder structure for the back-end and front-end, respectively. With all of the setup finished, let’s start writing our back-end code.
The first thing we’ll do is define the models for our regular database. In api > db
, open the schema.prisma
file. This is where we handle all of the database changes we need to make. Start by updating the provider
to postgresql
from sqlite
.
Then open your .env
file in the root of the project. You’ll see a commented-out reference to the DATABASE_URL
variable. Go ahead and uncomment this and update it to match your connection string to Postgres. It might look something like this.
DATABASE_URL=postgres://postgres:admin@localhost:5432/user_profiles
Code language: JavaScript (javascript)
Now if you go back to the schema.prisma
file, you’ll see where this value is used to connect to the database. The last thing we need to do in this file is add our profile model. There’s an example user model there and you can delete this and add the following code.
model Profile {
id String @id @default(uuid())
updatedAt DateTime
email String
blockchainAddress String
}
Code language: JavaScript (javascript)
That’s all we need for the database model. We can run a migration to get this table in the database now. To do that, run this command.
$ yarn rw prisma migrate dev
This will establish a connection to the database, prompt you for a migration name, then add your changes to the database. That’s all for the database. Now we’re going to show off one of Redwood’s commands to generate the types and resolvers for the GraphQL server we’ll use.
In order to create and update user profiles in the regular database, we’ll run this command to generate some code for us.
$ yarn rw g sdl --crud profile
This will add a new file inside api > src > graphql
that has all of the types to support our CRUD functionality. It also generates the resolvers to actually make database updates.
You can find the resolvers in api > src > services > profiles
. There are a couple of files related to testing in that directory, but if you open profiles.ts
, you’ll see all of the resolvers.
That wraps up everything on the back-end! Now we’re going to shift focus to the front-end, where we’ll write a smart contract and start interacting with a blockchain.
Before we start coding the user interface, we do need to get that smart contract in place. To do this, let’s install Truffle in the web
directory. This will let us interact with our local blockchain directly in the terminal. You can install it with the following command.
$ yarn add truffle web3
If that doesn’t work for you, try the following command:
$ npm install -g truffle
Sometimes there are issues with using Truffle if it’s not installed globally, but that might be dependent on your local setup.
Now that we have Truffle, let’s start making our smart contract by running the following command in the web
directory.
$ truffle init
This will create a new directory called contracts
and you’ll see an initial smart contract file written in Solidity. We’ll add another smart contract in this directory called Profile.sol
. This is how we’ll handle adding records to the blockchain.
Every Solidity file starts with the version you want to use. We’ll add this line to the top of the code.
pragma solidity ^0.5.0;
Code language: CSS (css)
Now we want to start writing the contract. Add the following code below the version declaration.
contract Profile {
}
Code language: CSS (css)
We can start defining some of the values we’ll be working with. Inside of the contract, add the following code.
uint256 public userCount = 0;
struct User {
uint256 id;
string name;
string role;
string profileImg;
bool isRegistered;
}
mapping(uint256 => User) public usersById;
Code language: PHP (php)
Solidity is an interesting mix of JavaScript/C++ syntax. The first variable we have is an integer that’s publically accessible called userCount
. This will help us track how many users we have and it’ll act as the index for their profile information.
Next, we have a struct that defines the User
and their profile info. A struct is similar to an interface or a type in TypeScript.
The next variable we have is a mapping. Mappings are like objects in JavaScript. We have a public mapping(object) called usersById
and it has a key-value pair of an integer and a User
object that looks something like this:
{
1: {
id: 1,
name: 'Spencer',
role: 'admin',
profileImg: 'https://res.cloudinary.com/milecia/image/upload/v1624811825/beach-360_p6u08j.jpg',
isRegistered: true
}
}
Code language: JavaScript (javascript)
With the variable definitions in place, we can add our constructor function. This will get executed exactly one time during the life of the smart contract when it’s initially deployed to the blockchain. Add this code below the mapping.
constructor() public {
createUser(
'Spencer',
'admin',
'https://res.cloudinary.com/milecia/image/upload/v1624811825/beach-360_p6u08j.jpg',
true
);
}
Code language: JavaScript (javascript)
We’re adding a new user profile to the blockchain as soon as we add this smart contract. That way we have some initial data to interact with. This is also where you can set the owner info for a smart contract to allow access to different functions, but that’s a more advanced topic.
For now we can add the createUser
function that is being referenced in the constructor. Right below the constructor call, add this:
function createUser(
string memory _name,
string memory _role,
string memory _profileImg,
bool _isRegistered
) public {
userCount++;
usersById[userCount] = User(
userCount,
_name,
_role,
_profileImg,
_isRegistered
);
}
Code language: JavaScript (javascript)
It’s very similar to a TypeScript function with a few differences. We want to be able to call this function from our front-end, so it will be public. Then we define the types for the function inputs. Inside the function, we’re incrementing the userCount
by 1.
Then we’re using the new userCount
value as the id for the new user profile. This function is going to let us add new user profiles from the front-end we’re about to build.
That’s all for the smart contract! Now we just need to deploy it.
Let’s write a quick migration script for our new smart contract. In the web > migrations
folder, add a new file called 2_deploy_profile_contracts.js
. Open that file and add the following code:
const Profile = artifacts.require("./Profile.sol");
module.exports = function(deployer) {
deployer.deploy(Profile);
};
Code language: JavaScript (javascript)
This is how we write migrations to deploy smart contract changes to the EVM. Now we need to actually run the deploy.
In your terminal, go to the web
directory and run:
$ truffle migrate
This will connect to the local EVM you have running with Ganache. If you haven’t opened Ganache, go ahead and do that and choose the “QuickStart” option.
You should see a couple of printouts in the terminal and the only thing you need to get is the contract address
of the deploy we wrote. It’ll look something like this: 0xe3173637950221539F40d7F54a431880786142BD
.
Now we can switch over to the front-end!
We need to set up a file to hold our configs to connect the smart contract we just deployed. Inside web > src
, create a new file called config.tsx
. Then add the following code:
export const PROFILE_ADDRESS = '0xe3173637950221539F40d7F54a431880786142BD'
Code language: JavaScript (javascript)
This is the contract address you got from the terminal after deploying the smart contract. Now we need to add the ABI (application binary interface) for the smart contract so that we can interact with the public functions and variables. You can find the ABI for the contract in web > build > Profile.json
. Go ahead and copy that abi
from that JSON file and paste it in the config.tsx
below the PROFILE_ADDRESS
.
export const PROFILE_ABI: any = [
{
"constant": true,
"inputs": [],
"name": "userCount",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x07973ccf"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "usersById",
"outputs": [
{
"name": "id",
"type": "uint256"
},
{
"name": "name",
"type": "string"
},
{
"name": "role",
"type": "string"
},
{
"name": "profileImg",
"type": "string"
},
{
"name": "isRegistered",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x426b5382"
},
{
"inputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor",
"signature": "constructor"
},
{
"constant": false,
"inputs": [
{
"name": "_name",
"type": "string"
},
{
"name": "_role",
"type": "string"
},
{
"name": "_profileImg",
"type": "string"
},
{
"name": "_isRegistered",
"type": "bool"
}
],
"name": "createUser",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0xe4f3ad95"
}
]
Code language: JavaScript (javascript)
That’s all for the setup. We can finally create the user interface for this Dapp!
In your terminal, go to the root of your project and run this command:
$ yarn rw g page Profile
This will generate a few new files for us and update the Routes.tsx
with a new /profile
route. This is the route users can see their profiles on. If you go to web > src > pages > ProfilePage
, you’ll see all the new files. You can take a look at the test file and the Storybook story, but our focus is on ProfilePage.tsx
.
Open this file and clear everything out. We’ll start fresh.
We’ll start by adding the packages we need to import and a type definition for the profile data. At the top of the file, add the following:
import { useState, useEffect } from 'react'
import { useMutation } from '@redwoodjs/web'
import Web3 from 'web3'
import { PROFILE_ABI, PROFILE_ADDRESS } from '../../config'
interface UserProps {
name: string;
role: string;
profileImg: string;
isRegistered: boolean;
}
Code language: JavaScript (javascript)
Then we’ll add the call definition to our GraphQL server.
Note: It’s common to see decentralized apps still using some kind of centralization. Right now, there aren’t many apps that are truly decentralized.
const CREATE_PROFILE_MUTATION = gql`
mutation CreateProfileMutation($input: CreateProfileInput!) {
createProfile(input: $input) {
id
}
}
`
Code language: PHP (php)
This gives us a way to access the database from the front-end. All that’s left now is to fill in the component.
We’ll start by declaring and exporting the component. After the GraphQL definition, add this code:
const ProfilePage = () => {
}
export default ProfilePage
Code language: JavaScript (javascript)
Now we’ll add the states we need right inside this component with this code:
const [createProfile] = useMutation(CREATE_PROFILE_MUTATION)
const [account, setAccount] = useState<string>('')
const [profile, setProfile] = useState<any>()
const [user, setUser] = useState<UserProps>()
Code language: JavaScript (javascript)
Next, we’re going to connect to the blockchain as soon as the app has loaded in the browser. We’ll do that by calling a function in a useEffect
hook like this:
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
const web3 = new Web3('http://localhost:7545')
const accounts = await web3.eth?.getAccounts()
setAccount(accounts[0])
const profile = new web3.eth.Contract(PROFILE_ABI, PROFILE_ADDRESS)
setProfile(profile)
const user = await profile.methods.usersById(3).call()
setUser(user)
}
Code language: JavaScript (javascript)
The loadData
function is how we get all of the information we need to interact with the blockchain. We make a new instance of Web3 that connects to the local EVM running in Ganache. Then we get the account id of the current user. We get access to the smart contract we deployed by using the address and ABI we go from our deploy.
Lastly, we specify an id and get the user profile info directly from the blockchain. Since we’ll be able to add new profiles to the blockchain and the database, we’ll have a form to let users enter their info. That means we’ll need a function to handle the form submission. We’ll add this code now, right below the loadData
function.
const handleSubmit = async (event) => {
event.preventDefault()
const { email, name, role, profileImg, isRegistered } = event.target.elements
const input = { email: email.value, updatedAt: new Date().toISOString(), blockchainAddress: account }
createProfile({
variables: { input },
})
await profile.methods.createUser(name.value, role.value, profileImg.value, isRegistered.value).send({ from: account, gas: 4712388 })
}
Code language: JavaScript (javascript)
This keeps the page from reloading while it pulls the data from the form and makes an input for the database and then calls the GraphQL resolver. Then we add the new profile to the blockchain with the createUser
method from the smart contract.
The last thing we need to do is render the HTML that will show in the browser. We’ll do this in the return statement for the component. So add this final snippet of code below the handleSubmit
function.
return (
<div>
<h1>Profile Page</h1>
Profile account id: {account}
{user &&
<div>
<p>{user.name}</p>
<input type="checkbox" checked={user.isRegistered} />
<p>{user.role}</p>
<img src={user.profileImg} width="360" />
</div>
}
<h2>Add user profile to the chain</h2>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor='name'>Name:</label>
<input name='name' type='text' />
</div>
<div>
<label htmlFor='email'>Email:</label>
<input name='email' type='email' />
</div>
<div>
<label htmlFor='role'>Role:</label>
<input name='role' type='text' />
</div>
<div>
<label htmlFor='profileImg'>Profile Pic:</label>
<input name='profileImg' type='text' />
</div>
<div>
<label htmlFor='isRegistered'>Registered:</label>
<input name='isRegistered' type='checkbox' />
</div>
<button type='submit'>Submit</button>
</form>
</div>
)
Code language: JavaScript (javascript)
This shows the data we retrieve from the blockchain if we have a user
defined. It also shows the form that we use to add new profiles to the blockchain and our database.
Here’s what it might look like in your browser.
We’re finally done with our Dapp!
If you want to take a look at the fully functioning front-end and back-end, you can clone the repo from the user-profile-dapp
folder in this repo. Or you can check out the front-end in this Code Sandbox.
Note: The Code Sandbox won’t show anything unless you set up your own config file. This one is connecting to a local EVM.
This is a great time to start learning how to work with blockchain and Web3 since it’s on the rise. It might be a niche technology, but it solves an interesting problem. Learning how to write smart contracts and build Dapps around them is a useful skill if you’re looking at where you might like to go with your career!