Introduction
In today’s digital age, privacy and identity are paramount. Yet, conventional systems are struggling to meet these demands. Enter blockchain, a technology that has the potential to revolutionize our world, placing a strong emphasis on privacy and identity. One such blockchain platform that stands out in this regard is Celo. Celo, with its emphasis on identity preservation, opens up numerous possibilities in decentralized application (DApp) development. In this tutorial, we will explore how to create an identity-preserving DApp using Celo and Flutter.
Prerequisites
Before starting this tutorial, it is recommended to have some prior knowledge and complete the following prerequisites:
- Basic understanding of blockchain technology (specifically CELO) and smart contracts.
- Familiarity with the Flutter framework and Dart language.
- Prior experience with EVM would be helpful, as Celo is fully compatible with Ethereum’s tooling and coding conventions.
If you haven’t met these prerequisites, I recommend you take the time to familiarize yourself with the basics of blockchain and Flutter development before proceeding.
Requirements
Before we begin, there are several tools that you need to have installed:
- Flutter: Our mobile app development framework. Install Flutter from HERE
- Dart SDK: Flutter uses Dart language. Ensure Dart SDK is installed by following the instructions HERE
- Celo Wallet: This will be needed to interact with the Celo blockchain. Install it from HERE
- Node.js: Ensure Node.js (version 12.0.1 or later) is installed. It can be downloaded from HERE
Writing the Smart Contract
We’ll be writing the smart contract for saving the address and phone number of users on the celo blockchain so as to securely preserve the identity of the user in a decentralized platform.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract Accounts {
// Mapping from hashed phone numbers to array of addresses
mapping (string => address[]) private accounts;
// Event emitted when a new address is added
event AddressAdded(string hashedPhoneNumber, address account);
// Add a new address to a phone number
function addAddress(string memory hashedPhoneNumber, address account) public {
// Ensure the address isn't null
require(account != address(0), "Invalid address");
// Check that the address isn't already added
address[] memory associatedAddresses = accounts[hashedPhoneNumber];
for (uint256 i = 0; i < associatedAddresses.length; i++) {
require(associatedAddresses[i] != account, "Address already added");
}
// Add the address
accounts[hashedPhoneNumber].push(account);
// Emit an event
emit AddressAdded(hashedPhoneNumber, account);
}
// Get the addresses associated with a phone number
function lookupAccounts(string memory hashedPhoneNumber) public view returns (address[] memory) {
return accounts[hashedPhoneNumber];
}
}
This contract includes a mapping from hashed phone numbers (stored as strings) to arrays of addresses.
- The
mapping
links hashed phone numbers to an array of Ethereum addresses related to them. - The
event AddressAdded
triggers and logs when a new address is added to a hashed phone number. - The
addAddress
function allows an address to be added to the list of addresses associated with a hashed phone number, provided the address isn’t null and hasn’t already been added. - The
lookupAccounts
function returns the list of addresses associated with a hashed phone number.
Deploy the Smart Contract
We will be compiling and testing our smart contract using 2 methods which have been deeply explained in some tutorials in the academy:
- Deploying smart contract on remix: Check the
Creating and Deploying a Celo Smart Contract
section of this tutorial. - Deploying smart contract using Celo Composer and terminal: Check the
Building the Smart Contract
section of this tutorial.
Getting started with Flutter and Celo
We will begin by setting up our development environment and configuring a new Flutter project for Celo. Subsequently, we will delve into the specifics of integrating Celo’s identity protocol into our DApp.
Setting up Flutter Project
Start by initializing a new Flutter project by executing the following command in your terminal:
flutter create identity_preserving_dapp
Next, navigate into your new Flutter project:
cd identity_preserving_dapp
Configuring the Project for Celo
To interact with Celo’s network, we need to install a couple of packages. We’re going to use riverpod
, web3dart
and http
for this tutorial. riverpod is used for managing the state of the Dapp, web3dart is a Dart library that allows us to interact with Ethereum compatible chains like Celo, and http is used to make HTTP requests.
Add the following dependencies to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
web3dart: ^2.6.1
http: ^1.1.0
flutter_ riverpod: ^2.3.4
flutter_dotenv: ^5.0.2
Then, install these dependencies by running the following command in your terminal:
flutter pub get
We’ll use Celo’s Alfajores test network for this tutorial. To connect to it, you need to set the RPC URL as an environment variable. Create a .env
file in the root of your project and add the following line:
CELO_RPC_URL=https://alfajores-forno.celo-testnet.org
You’ll also need your Celo account’s private key. Warning: Never share your private key and never commit it into version control. For the purposes of this tutorial, add it to your .env file like this:
CELO_PRIVATE_KEY=YOUR_PRIVATE_KEY
Now your project is configured for Celo!
Integrating Celo’s Identity Protocol
With the project setup complete, it’s time to integrate Celo’s identity protocol. For simplicity, we’ll focus on the phone number verification aspect of the protocol.
Celo’s identity protocol is built around the Accounts
smart contract. This contract maps addresses to metadata, including phone numbers.
- First, let’s import the necessary libraries in your
celo_id.dart
Dart file:
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart' as dotenv;
- Initialize your web3 client using the RPC URL from the environment variable:
var apiUrl = dotenv.env['CELO_RPC_URL'];
final client = Web3Client(apiUrl, Client());
- Get your account credentials using your private key:
final credentials = await client.credentialsFromPrivateKey(dotenv.env['CELO_PRIVATE_KEY']);
- Let’s create a function called
addPhoneNumberAddress
that takes the user address and phone number, encrypt the number and then pass it to the smart contract for mapping.
Future addPhoneNumberAddress(String phoneNumber, String address) async {
// final client = Web3Client(apiUrl ?? "", Client());
final contract = await deployedAccountContract;
// Hash the phone number
var phoneNumberHash =
keccak256(Uint8List.fromList(phoneNumber.codeUnits)).toString();
print("phone number ===>> $phoneNumberHash");
// Get the addAddress function from the contract
final addAddressFunction = contract.function('addAddress');
// Call the addAddress function
final response = await client.sendTransaction(
credentials,
Transaction.callContract(
contract: contract,
function: addAddressFunction,
parameters: [phoneNumberHash, EthereumAddress.fromHex(address)],
),
chainId: 44787);
final dynamic receipt = await awaitResponse(response);
return receipt;
}
- Now let’s get the address linked to a phone number. We’ll use the
lookupAccounts
method of theAccounts
contract. This method takes the phone number hash and returns a list of addresses associated with it.
Future<List<EthereumAddress>> lookupAccounts(String phoneNumber) async {
// Hash the phone number
var phoneNumberHash = keccak256(Uint8List.fromList(phoneNumber.codeUnits));
// Get the Accounts contract
var accountsContract = DeployedContract(
ContractAbi.fromJson('ACCOUNTS_CONTRACT_ABI', 'Accounts'),
EthereumAddress.fromHex('ACCOUNTS_CONTRACT_ADDRESS'));
// Get the lookupAccounts function
var lookupAccountsFunction = accountsContract.function('lookupAccounts');
// Call the function
var result = await client.call(
contract: accountsContract,
function: lookupAccountsFunction,
params: [phoneNumberHash],
);
// Parse the result
var addresses = result[0] as List<EthereumAddress>;
return addresses;
}
- Replace
ACCOUNTS_CONTRACT_ABI
andACCOUNTS_CONTRACT_ADDRESS
with the actual ABI and address of theAccounts
contract.
Here is the full code:
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
var apiUrl = dotenv.env['CELO_RPC_URL'];
final client = Web3Client(apiUrl ?? "", Client());
class CeloPreservingIdentity {
EthPrivateKey get credentials {
var credentials =
EthPrivateKey.fromHex(dotenv.env['CELO_PRIVATE_KEY'] ?? "");
// var address = credentials.address;
return credentials;
}
/// Get deployed Accounts contract
Future<DeployedContract> get deployedAccountContract async {
const String abiDirectory = 'lib/service/contract.abi.json';
String contractABI = await rootBundle.loadString(abiDirectory);
final DeployedContract contract = DeployedContract(
ContractAbi.fromJson(contractABI, 'Accounts'),
EthereumAddress.fromHex(dotenv.env['CONTRACT_ADDRESS'] ?? ""),
);
return contract;
}
Future addPhoneNumberAddress(String phoneNumber, String address) async {
// final client = Web3Client(apiUrl ?? "", Client());
final contract = await deployedAccountContract;
// Hash the phone number
var phoneNumberHash =
keccak256(Uint8List.fromList(phoneNumber.codeUnits)).toString();
print("phone number ===>> $phoneNumberHash");
// Get the addAddress function from the contract
final addAddressFunction = contract.function('addAddress');
// Call the addAddress function
final response = await client.sendTransaction(
credentials,
Transaction.callContract(
contract: contract,
function: addAddressFunction,
parameters: [phoneNumberHash, EthereumAddress.fromHex(address)],
),
chainId: 44787);
final dynamic receipt = await awaitResponse(response);
return receipt;
}
Future<List<dynamic>> lookupAccounts(String phoneNumber) async {
// Hash the phone number
var phoneNumberHash =
keccak256(Uint8List.fromList(phoneNumber.codeUnits)).toString();
// Get the Accounts contract
var accountsContract = await deployedAccountContract;
// Get the lookupAccounts function
var lookupAccountsFunction = accountsContract.function('lookupAccounts');
// Call the function
var result = await client.call(
contract: accountsContract,
function: lookupAccountsFunction,
params: [phoneNumberHash],
);
// Parse the result
print("result ===>> ${result[0]}");
List addresses = result[0];
return addresses;
}
Future<dynamic> awaitResponse(dynamic response) async {
int count = 0;
while (true) {
final TransactionReceipt? receipt =
await client.getTransactionReceipt(response);
if (receipt != null) {
print('receipt ===>> $receipt');
return receipt.logs[0].data;
}
// Wait for a while before polling again
await Future<dynamic>.delayed(const Duration(seconds: 1));
if (count == 6) {
return null;
} else {
count++;
}
}
}
}
Now you have integrated Celo’s identity protocol into your Flutter DApp! Users can input their phone number, and the DApp will return the Celo addresses linked to that number.
Now that you have a basic understanding of how to look up accounts linked to a specific phone number, let’s add a user interface to our Flutter app to use this functionality.
Open the file lib/main.dart
and replace its contents with the following code:
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:identity_preserving_dapp/screen/home_page.dart';
void main() async {
await dotenv.load(fileName: '.env');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: 'Celo Identity DApp',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
),
);
}
}
The source code contains the whole flow of the tutorial in code. The Screen
directory contains the view or the uuser interface of the dApp, the controller connects the view to the web3 helper class named CeloPreservingIdentity
class which connects to the smart contract through web3dart
This code creates a simple Flutter app with a text field to enter a phone number and a button to look up the associated Celo accounts. It will display the addresses of the accounts linked to the input phone number.
Remember to replace ACCOUNTS_CONTRACT_ABI
and ACCOUNTS_CONTRACT_ADDRESS
with the actual ABI and address of the Accounts contract.
This is a basic implementation and can be expanded according to your needs. With this knowledge, you can integrate Celo’s identity protocol into your DApp in a way that fits your use case. Remember to always handle users’ phone numbers with care to respect their privacy.
This is the UI of the dApp:
Conclusion
By following this tutorial, you’ve built a decentralized mobile application using Flutter and Celo. We started from setting up a development environment for Flutter, configuring a new Flutter project for Celo, and finally integrating Celo’s identity protocol into our DApp.
Congratulations on building an identity-preserving DApp with Celo and Flutter!
Next Steps
Now that you’ve built an identity-preserving DApp, why not explore other features of Celo? Here are some ideas for further learning:
- Building a payment DApp using Celo
- Implementing Celo’s stablecoin in your DApp
- Adding more complex smart contract functionalities to your DApp
About the Author
John Igwe Eke, a proficient mobile developer and a technical writer, combines his expertise in software development with his enthusiasm for blockchain and Web3 technologies. He leverages this unique blend of skills to distill complex concepts into understandable, engaging content, thereby aiding in the democratization of these emerging technologies. Connect with me on twitter, LinkedIn, and Github.
References
- Flutter Official Documentation
- Celo Official Documentation
- Celo’s Identity Protocol
- Dart Language Tour
- Source Code