Working with User Profiles on a Blockchain

Milecia

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.

Setting things up for the project

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.

1$ 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.

Working on the back-end

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.

1DATABASE_URL=postgres://postgres:admin@localhost:5432/user_profiles

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.

1model Profile {
2 id String @id @default(uuid())
3 updatedAt DateTime
4 email String
5 blockchainAddress String
6}

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.

1$ 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.

Working with GraphQL

In order to create and update user profiles in the regular database, we'll run this command to generate some code for us.

1$ 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.

Writing the smart contract

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.

1$ yarn add truffle web3

If that doesn't work for you, try the following command:

1$ 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.

1$ 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.

1pragma solidity ^0.5.0;

Now we want to start writing the contract. Add the following code below the version declaration.

1contract Profile {
2}

We can start defining some of the values we'll be working with. Inside of the contract, add the following code.

1uint256 public userCount = 0;
2
3struct User {
4 uint256 id;
5 string name;
6 string role;
7 string profileImg;
8 bool isRegistered;
9}
10
11mapping(uint256 => User) public usersById;

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{
2 1: {
3 id: 1,
4 name: 'Spencer',
5 role: 'admin',
6 profileImg: 'https://res.cloudinary.com/milecia/image/upload/v1624811825/beach-360_p6u08j.jpg',
7 isRegistered: true
8 }
9}

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.

1constructor() public {
2 createUser(
3 'Spencer',
4 'admin',
5 'https://res.cloudinary.com/milecia/image/upload/v1624811825/beach-360_p6u08j.jpg',
6 true
7 );
8}

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:

1function createUser(
2 string memory _name,
3 string memory _role,
4 string memory _profileImg,
5 bool _isRegistered
6) public {
7 userCount++;
8
9 usersById[userCount] = User(
10 userCount,
11 _name,
12 _role,
13 _profileImg,
14 _isRegistered
15 );
16}

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.

Writing the migration

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:

1const Profile = artifacts.require("./Profile.sol");
2
3module.exports = function(deployer) {
4 deployer.deploy(Profile);
5};

This is how we write migrations to deploy smart contract changes to the EVM. Now we need to actually run the deploy.

Deploying the smart contract

In your terminal, go to the web directory and run:

1$ 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!

Setting the smart contract configs

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:

1export const PROFILE_ADDRESS = '0xe3173637950221539F40d7F54a431880786142BD'

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.

1export const PROFILE_ABI: any = [
2 {
3 "constant": true,
4 "inputs": [],
5 "name": "userCount",
6 "outputs": [
7 {
8 "name": "",
9 "type": "uint256"
10 }
11 ],
12 "payable": false,
13 "stateMutability": "view",
14 "type": "function",
15 "signature": "0x07973ccf"
16 },
17 {
18 "constant": true,
19 "inputs": [
20 {
21 "name": "",
22 "type": "uint256"
23 }
24 ],
25 "name": "usersById",
26 "outputs": [
27 {
28 "name": "id",
29 "type": "uint256"
30 },
31 {
32 "name": "name",
33 "type": "string"
34 },
35 {
36 "name": "role",
37 "type": "string"
38 },
39 {
40 "name": "profileImg",
41 "type": "string"
42 },
43 {
44 "name": "isRegistered",
45 "type": "bool"
46 }
47 ],
48 "payable": false,
49 "stateMutability": "view",
50 "type": "function",
51 "signature": "0x426b5382"
52 },
53 {
54 "inputs": [],
55 "payable": false,
56 "stateMutability": "nonpayable",
57 "type": "constructor",
58 "signature": "constructor"
59 },
60 {
61 "constant": false,
62 "inputs": [
63 {
64 "name": "_name",
65 "type": "string"
66 },
67 {
68 "name": "_role",
69 "type": "string"
70 },
71 {
72 "name": "_profileImg",
73 "type": "string"
74 },
75 {
76 "name": "_isRegistered",
77 "type": "bool"
78 }
79 ],
80 "name": "createUser",
81 "outputs": [],
82 "payable": false,
83 "stateMutability": "nonpayable",
84 "type": "function",
85 "signature": "0xe4f3ad95"
86 }
87]

That's all for the setup. We can finally create the user interface for this Dapp!

Making the user interface

In your terminal, go to the root of your project and run this command:

1$ 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.

Adding imports and type definitions

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:

1import { useState, useEffect } from 'react'
2import { useMutation } from '@redwoodjs/web'
3import Web3 from 'web3'
4import { PROFILE_ABI, PROFILE_ADDRESS } from '../../config'
5
6interface UserProps {
7 name: string;
8 role: string;
9 profileImg: string;
10 isRegistered: boolean;
11}

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.

1const CREATE_PROFILE_MUTATION = gql`
2 mutation CreateProfileMutation($input: CreateProfileInput!) {
3 createProfile(input: $input) {
4 id
5 }
6 }
7`

This gives us a way to access the database from the front-end. All that's left now is to fill in the component.

Filling in the ProfilePage component

We'll start by declaring and exporting the component. After the GraphQL definition, add this code:

1const ProfilePage = () => {
2
3}
4
5export default ProfilePage

Now we'll add the states we need right inside this component with this code:

1const [createProfile] = useMutation(CREATE_PROFILE_MUTATION)
2
3const [account, setAccount] = useState<string>('')
4const [profile, setProfile] = useState<any>()
5const [user, setUser] = useState<UserProps>()

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:

1useEffect(() => {
2 loadData()
3}, [])
4
5const loadData = async () => {
6 const web3 = new Web3('http://localhost:7545')
7
8 const accounts = await web3.eth?.getAccounts()
9 setAccount(accounts[0])
10
11 const profile = new web3.eth.Contract(PROFILE_ABI, PROFILE_ADDRESS)
12 setProfile(profile)
13
14 const user = await profile.methods.usersById(3).call()
15 setUser(user)
16}

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.

1const handleSubmit = async (event) => {
2 event.preventDefault()
3
4 const { email, name, role, profileImg, isRegistered } = event.target.elements
5
6 const input = { email: email.value, updatedAt: new Date().toISOString(), blockchainAddress: account }
7
8 createProfile({
9 variables: { input },
10 })
11
12 await profile.methods.createUser(name.value, role.value, profileImg.value, isRegistered.value).send({ from: account, gas: 4712388 })
13}

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.

1return (
2 <div>
3 <h1>Profile Page</h1>
4 Profile account id: {account}
5 {user &&
6 <div>
7 <p>{user.name}</p>
8 <input type="checkbox" checked={user.isRegistered} />
9 <p>{user.role}</p>
10 <img src={user.profileImg} width="360" />
11 </div>
12 }
13 <h2>Add user profile to the chain</h2>
14 <form onSubmit={handleSubmit}>
15 <div>
16 <label htmlFor='name'>Name:</label>
17 <input name='name' type='text' />
18 </div>
19 <div>
20 <label htmlFor='email'>Email:</label>
21 <input name='email' type='email' />
22 </div>
23 <div>
24 <label htmlFor='role'>Role:</label>
25 <input name='role' type='text' />
26 </div>
27 <div>
28 <label htmlFor='profileImg'>Profile Pic:</label>
29 <input name='profileImg' type='text' />
30 </div>
31 <div>
32 <label htmlFor='isRegistered'>Registered:</label>
33 <input name='isRegistered' type='checkbox' />
34 </div>
35 <button type='submit'>Submit</button>
36 </form>
37 </div>
38)

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!

Finished code

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.

Conclusion

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!

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.