Introduction
Celo is a blockchain platform that provides a decentralized identity (DID) system, allowing users to securely manage their identities and interact with various services. In this tutorial, we’ll learn how to integrate Celo’s DID system into a mobile app using Flutter and the Celo WalletConnect SDK. By doing so, we can leverage Celo’s secure and inclusive identity infrastructure to enhance the functionality and security of our mobile app.
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, make sure you have the following requirements met:
- Flutter Development Environment: Ensure that you have set up a Flutter development environment on your machine. You can follow the official Flutter installation guide for your operating system to get started.
- Solidity: We will use Solidity for writing smart contracts.
- A Celo Wallet: Make sure to set up a wallet on Celo’s Alfajores Testnet. You can do this by following the steps from the
Setting up a Celo Wallet
section in this tutorial. - Fund your wallet using the Celo Faucet. To do this, copy the wallet address and visit the Celo Faucet. Then paste your wallet address into the designated field and confirm.
- Ensure Nodejs is installed on your computer. If not, download it from here and install it on your computer following the instructions given.
You can check out this video on how to install and set up the celo-wallet-extension on your computer.
Project Initialization
- In your terminal run
npx @celo/celo-composer@latest create
This will prompt you to select the framework and template you want to use. In this tutorial, we’ll be using the Flutter as the front-end framework.
- Once you have selected the Futter framework, you will be asked to select the smart contract framework which is the smart-contract development tool we’ll be using for this tutorial.
Select Hardhat for this tutorial.
- After selecting the tool, you will be asked if you want subgraph support for your dApp. Select “No” as we will not be using a subgraph for the dApp in this tutorial.
- Finally, you will be asked to enter the name of the project. Enter the project name of your choice and press the enter key.
Once it’s successfully created, open the project folder you created in the code editor (i’ll be using VSCode). The folder structure should look like this:
In the packages folder, you should see the flutter-app and hardhat folders. The hardhat folder contains the hardhat-project with the necessary setup needed to create and deploy our simple smart contract while the flutter-app folder contains the flutter starter files for our DApp.
- Navigate to your root directory in your terminal to run this command to install the dependencies in the package.json files.
yarn install
Note that you must have yarn already installed on your machine.
After all dependencies have been installed, we can now go ahead to create our smart contract.
Building the Smart Contract
The contract folder in the hardhat folder contains some solidity files, go ahead and delete the files. Then create a new file in the contracts folder and name it RegisterDId.sol
In the RegisterDid.sol
file, copy and paste this code.
pragma solidity ^0.8.6;
contract CeloDIDRegistry {
struct Identity {
bytes32 did;
string identifier;
bool isVerified;
}
mapping(address => Identity) identities;
mapping(address => bool) addressExists;
event DIDSet(address indexed wallet, bytes32 did);
event DIDChanged(address indexed wallet, bytes32 newDid);
event DIDVerified(address indexed wallet);
// 1. Register DID
function registerDID(address _address, string memory _did, string memory _identifier) public returns (bytes32) {
require(!addressExists[_address], "Address already registered");
require(!identities[_address].isVerified, "DID already registered and verified");
bytes32 did = keccak256(abi.encodePacked(_did));
identities[_address] = Identity(did, _identifier, false);
addressExists[_address] = true;
emit DIDSet(_address, did);
return did;
}
// 2. Change DID
function changeDID(address _address, bytes32 _currentDID, string memory _newDID, string memory _identifier, string memory _newIdentifier) public returns (bytes32){
require(addressExists[_address], "Address not registered");
require(identities[_address].did == _currentDID, "Invalid current DID");
require(identities[_address].isVerified, "Not verified");
require(keccak256(abi.encodePacked(identities[_address].identifier)) == keccak256(abi.encodePacked(_identifier)), "Invalid identifier");
bytes32 newDID = keccak256(abi.encodePacked(_newDID));
identities[_address].did = newDID;
identities[_address].identifier = _newIdentifier;
emit DIDChanged(_address, newDID);
return newDID;
}
// 3. Verify DID
function verifyDID(address _address, bytes32 _did, string memory _identifier) public returns (bool){
require(addressExists[_address], "Address not registered");
require(identities[_address].did == _did, "Invalid DID");
require(!identities[_address].isVerified, "DID already registered and verified");
require(keccak256(abi.encodePacked(identities[_address].identifier)) == keccak256(abi.encodePacked(_identifier)), "Invalid identifier");
identities[_address].isVerified = true;
emit DIDVerified(_address);
return identities[_address].isVerified;
}
// 4. Get DID by Address
function getDIDByAddress(address _address, string memory _identifier) public view returns (bytes32) {
require(identities[_address].did != 0, "Address does not exist");
require(keccak256(abi.encodePacked(identities[_address].identifier)) == keccak256(abi.encodePacked(_identifier)), "Invalid identifier");
return identities[_address].did;
}
// 5. Authenticate User
function authenticateUser(address _address, bytes32 _did) public view returns (bool) {
require(addressExists[_address], "Address not registered");
require(identities[_address].isVerified, "User is not verified");
if (identities[_address].did == _did) {
return true;
}
return false;
}
}
- We declared the contract and called it
CeloDIDRegistry
, which consists the struct, maps, events and the functions we’ll be using in this tutorial
struct Identity {
bytes32 did;
string identifier;
bool isVerified;
}
- Then we created a struct named
Identity
which defines the properties of the DID. It contains the user’s DID in bytes32 form, and the isVerified boolean property to determine if a user is verified or not.
mapping(address => Identity) identities;
mapping(address => bool) addressExists;
event DIDSet(address indexed wallet, bytes32 did);
event DIDChanged(address indexed wallet, bytes32 newDid);
event DIDVerified(address indexed wallet)
-
We created the
identities
mapping which associates or maps the user wallet address to the Identity which contains the DID and the isVerified property.
We also created theaddressExists
mapping which associates or maps the user wallet address to a boolean value that shows if an address exists or not. -
Then the
DIDSet
event is emitted when the DID is registered by the user, theDIDChanged
event is emitted when the user changes their DID and theDIDVerified
event is emitted when the user verifies their DID using their wallet address.
// 1. Register DID
function registerDID(address _address, string memory _did, string memory _identifier) public returns (bytes32) {
require(!addressExists[_address], "Address already registered");
require(!identities[_address].isVerified, "DID already registered and verified");
bytes32 did = keccak256(abi.encodePacked(_did));
identities[_address] = Identity(did, _identifier, false);
addressExists[_address] = true;
emit DIDSet(_address, did);
return did;
}
// 2. Change DID
function changeDID(address _address, bytes32 _currentDID, string memory _newDID, string memory _identifier, string memory _newIdentifier) public returns (bytes32){
require(addressExists[_address], "Address not registered");
require(identities[_address].did == _currentDID, "Invalid current DID");
require(identities[_address].isVerified, "Not verified");
require(keccak256(abi.encodePacked(identities[_address].identifier)) == keccak256(abi.encodePacked(_identifier)), "Invalid identifier");
bytes32 newDID = keccak256(abi.encodePacked(_newDID));
identities[_address].did = newDID;
identities[_address].identifier = _newIdentifier;
emit DIDChanged(_address, newDID);
return newDID;
}
-
The
registerDID
function takes in the user wallet address, the did and the user’s unique identifier, hashes the did and registers it in the map created. Checks are done to know if the DID has already been registered. -
Also the
ChangeDID
function takes in the user address, the current or old DID, the new did(to be hashed) and the user’s current and new unique identifiers. The function checks if the address exists, it also checks if the current did and identifier match the did and identifier mapped or associated to the address and then it hashes and replaces the old did with the new did which comes in the format
did:celo:{your identifier}
.
// 3. Verify DID
function verifyDID(address _address, bytes32 _did, string memory _identifier) public returns (bool){
require(addressExists[_address], "Address not registered");
require(identities[_address].did == _did, "Invalid DID");
require(!identities[_address].isVerified, "DID already registered and verified");
require(keccak256(abi.encodePacked(identities[_address].identifier)) == keccak256(abi.encodePacked(_identifier)), "Invalid identifier");
identities[_address].isVerified = true;
emit DIDVerified(_address);
return identities[_address].isVerified;
}
// 4. Get DID by Address
function getDIDByAddress(address _address, string memory _identifier) public view returns (bytes32) {
require(identities[_address].did != 0, "Address does not exist");
require(keccak256(abi.encodePacked(identities[_address].identifier)) == keccak256(abi.encodePacked(_identifier)), "Invalid identifier");
return identities[_address].did;
}
-
The
verifyDID
function takes in the user’s address, the user’s Celo did and user’s identifier and then changes the value of the isVerified property of the Identity associated to the wallet address provided to true. Necessary checks are done to ensure the address exists. -
The
getDIDByAddress
function takes in the user’s address and the user’s unique identifier, compares the entered identifier to the one registered on the contract with the address, and returns the Celo DID associated with the address.
// 5. Authenticate User
function authenticateUser(address _address, bytes32 _did) public view returns (bool) {
require(addressExists[_address], "Address not registered");
require(identities[_address].isVerified, "User is not verified");
if (identities[_address].did == _did) {
return true;
}
return false;
}
- The
authenticateUser
function takes in the user’s address and celo DID, checks if the user exists and the DID entered is mapped to the user’s address on the contract.
Compiling the Smart Contract
Before deploying your smart contract to the Celo blockchain, you will have to compile the smart contract using a command in your terminal.
- To compile the smart contract, run these commands to navigate t the hardhat directory from the root directory of your project.
cd packages
cd hardhat
- Then create a
.env
file in the hardhat folder and add your MNEMONIC (phrases) key in the .env file or make use of your private key.
MNEMONIC=//add your wallet seed phrase here
PRIVATE_KEY= ‘<your private key here>’
-
Install the
dotenv package
to be able to import the env file and use it in the config.
Enter this command in your terminal to install the dotenv package
yarn add dotenv
-
Next, open the
hardhat.config.js
file and replace the content of the file with the code below:
Note that the solidity version depends on the pragma version you used in your smart contract file.
require("@nomicfoundation/hardhat-chai-matchers")
require('dotenv').config({path: '.env'});
require('hardhat-deploy');
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545"
},
alfajores: {
gasPrice: 200000000000,
gas: 41000000000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: [process.env.PRIVATE_KEY],
// {
// mnemonic: process.env.MNEMONIC,
// path: "m/44'/52752'/0'/0"
// },
chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0"
},
chainId: 42220
},
},
solidity: "0.8.6",
};
- Now to compile the contract, run this command in your terminal.
npx hardhat compile
- Once the compilation is successful, you should see the message below on your terminal:
Deploying the Smart Contract
We will be deploying the smart contract to the Alfajores testnet. To do this,
- Replace the content of the
00-deploy.js
in the deploy folder with the code below:
const hre = require("hardhat");
async function main() {
const CeloDIDRegistry = await hre.ethers.getContractFactory("CeloDIDRegistry");
const celoDIDRegistry = await CeloDIDRegistry.deploy();
await celoDIDRegistry.deployed();
console.log("CeloDIDRegistry deployed to:", celoDIDRegistry.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
- Then, run the following command in your terminal to deploy the smart contract to the Celo blockchain.
npx hardhat run deploy/00-deploy.js --network alfajores
-
Save the smart contract address that will be printed on your terminal when deployment is successful somewhere because you will need it in the course of the tutorial.
-
For the purpose of this tutorial, inside the flutter project, create a .env file, create variables inside the .env file called “
CONTRACT_ADDRESS
” and “PRIVATE_KEY
”, then assign the address returned after deploying your smart contract to celo alfajores network. -
Also copy the contract abi from
packages/hardhat/artifacts/contracts/RegisterDid.sol/CeloDIDRegistry.json
file, and then create a new file inside your flutter project called “did.abi.json”, paste the ABI copied inside this file.
In this tutorial, thedid.abi.json
file is located inside theflutter-app/lib/module/services
folder.
Building the DApp/User Interface
-
We will be building a simple UI where the user will have to register the wallet and generate a Celo DID through the unique identifier they enter.
-
Then we’ll build the screen to interact with other DApps or smart contracts and make a decentralized payment using the generated Celo DID as a means of authentication for the user. This keeps the DApp secure and allows verified and authorized users to interact with the DApps and also make a secured payment on the platform.
-
We’ll build the Register DID screen, this screen enables users to register a new wallet address and generates a Celo DID to be used as a means of authentication for interaction on the platform.
-
We’ll also build the screen to verify your DID, this should serve as a double check on the DID initially registered, users must verify their DID before being authorized to interact with any smart contract/DApps or making payment.
-
We’ll also build the UI for users to change their DID, the user will be prompted to enter their wallet address, the current DID, the current identifier and then the new unique identifier to create a new DID for them.
-
We’ll lastly build the screen for users to view their DID by entering their wallet address and their unique identifier. This identifier serves as a security to ensure other people who have access to your address don’t access your Celo DID.
Now let’s get started.
- Ensure you navigate to the flutter project folder from the root folder by running the following commands:
cd packages
cd flutter-app
- We’ll start by adding the dependencies to the pubspec.yaml file or run the command below in your terminal.
To keep it simple, we will be making use of the flutter_riverpod package for state management instead of the proposed flutter_bloc proposed by Celo composer.
So remove the flutter_block package from your dependencies and run the commands below
flutter pub add flutter_riverpod
flutter pub upgrade
- Replace the code in your
main.dart
file with the code below as this serves as the root of our flutter application:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_celo_composer/configs/themes.dart';
import 'package:flutter_celo_composer/module/services/secure_storage.dart';
import 'package:flutter_celo_composer/module/view/screens/authentication_screen.dart';
import 'package:flutter_celo_composer/module/view/screens/home_page.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stack_trace/stack_trace.dart' as stack_trace;
void main() async {
await dotenv.load(fileName: '.env');
runApp(const MyApp());
FlutterError.demangleStackTrace = (StackTrace stack) {
if (stack is stack_trace.Trace) return stack.vmTrace;
if (stack is stack_trace.Chain) return stack.toTrace().vmTrace;
return stack;
};
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: buildDefaultTheme(context),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String? isCreated;
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) async {
isCreated = await UserSecureStorage().getCreatedBoolean();
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return isCreated != null && isCreated == 'true'
? const HomePage()
: const AuthenticationScreen();
}
}
- We will be creating five(5) folders inside the
module folder
, these include theVIEW
folder that contains the User Interfaces (Screens) of the app and the widgets, theVIEW_MODEL
which contains the providers and the controllers of the application, theMODEL
folder which contains the repository folder for interacting with libraries, packages and services, theCUSTOM_WIDGETS
folder that contains all the custom widgets used in the app and finally we’ll have theSERVICES
folder which will contain the smart contract interactions, the abi json file and other services needed in the app.
The project structure should look like this:
Users will not be redirected to any wallet app to process any payment as the purpose of the tutorial is to show users how to create a Celo decentralized identity using the Celo did prefix: did:celo:<identifier>
, hashing it on the smart contract deployed to Celo blockchain and the mapping it to the address of the user with the unique identifier of the user. This makes any activity on the DApp secure as users now have a personalized decentralized identity and can now be authenticated before proceeding with any crucial activity on the DApp.
Registration Screen
This screen contains the user wallet address field which collects the user’s wallet address. It also asks the user to select the identifier for generating the Celo identity and then displays a field for the user to enter the unique identity value.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_celo_composer/module/custom_widgets/snack_bar.dart';
import 'package:flutter_celo_composer/module/models/celo_model.dart';
import 'package:flutter_celo_composer/module/services/secure_storage.dart';
import 'package:flutter_celo_composer/module/view/screens/home_page.dart';
import 'package:flutter_celo_composer/module/view/widgets/custom_button.dart';
import 'package:flutter_celo_composer/module/view/widgets/custom_dropdown.dart';
import 'package:flutter_celo_composer/module/view/widgets/custom_textfield.dart';
import 'package:flutter_celo_composer/module/view_model/controllers/celo_controller.dart';
import 'package:flutter_celo_composer/module/view_model/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AuthenticationScreen extends ConsumerStatefulWidget {
const AuthenticationScreen({super.key});
@override
ConsumerState<AuthenticationScreen> createState() =>
_AuthenticationScreenState();
}
class _AuthenticationScreenState extends ConsumerState<AuthenticationScreen> {
TextEditingController walletController = TextEditingController();
TextEditingController identifierController = TextEditingController();
String? text;
bool obscureText = true;
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(
left: size.width * 0.04, right: size.width * 0.04, top: 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Image.asset('assets/images/logo.png',
height: 60, width: 90),
const SizedBox(width: 10),
const Text(
'DECENTRALIZED ID',
maxLines: 1,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: Colors.black54),
)
],
),
Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'Enter your details to generate your unique celo DID which serves as a means of authentication on this app.',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.black),
),
const SizedBox(height: 30),
CustomTextField(
text: 'Wallet Address',
controller: walletController,
hint: 'enter your wallet address',
),
const SizedBox(height: 15),
DropDownCustom(
text: 'Unique Identifier',
value: text,
onChanged: (dynamic value) {
setState(() {
text = value;
});
},
hint: 'Select an identifier',
items: const <String>[
'Full Name',
'Password',
'Secret Key',
'Others'
],
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: text != null
? CustomTextField(
text: text ?? '',
obscureText: obscureText,
controller: identifierController,
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscureText = !obscureText;
});
},
icon: Icon(
!obscureText
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black26,
)),
hint: text != 'Others'
? 'enter your ${text!.toLowerCase()}'
: 'enter your unique identifier',
)
: null,
)
],
),
),
const SizedBox(height: 50),
CustomButtonWidget(
text: ref.watch(celoProvider).createStatus == Status.loading
? const CircularProgressIndicator(color: Colors.white)
: const Text(
'Generate CELO DID',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.white),
),
onPressed: () async {
if (walletController.text.trim().isEmpty || text == null) {
CustomSnackbar.responseSnackbar(context, Colors.redAccent,
'Fill the required fields..');
return;
}
CeloDIDModel data = CeloDIDModel(
address: walletController.text.trim(),
identifier: identifierController.text.trim());
await ref.read(celoProvider).createCeloDID(data, context);
},
),
const SizedBox(height: 10),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Already registered ? ',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.black),
children: <TextSpan>[
TextSpan(
text: 'Proceed to Home',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black54),
recognizer: TapGestureRecognizer()
..onTap = () async {
await UserSecureStorage()
.setCreatedBoolean();
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute<dynamic>(
builder: (_) => const HomePage()));
})
])),
)
],
),
),
),
),
);
}
@override
void dispose() {
walletController.dispose();
identifierController.dispose();
super.dispose();
}
}
onPressed: () async {
if (walletController.text.trim().isEmpty || text == null) {
CustomSnackbar.responseSnackbar(context, Colors.redAccent,
'Fill the required fields..');
return;
}
CeloDIDModel data = CeloDIDModel(
address: walletController.text.trim(),
identifier: identifierController.text.trim());
await ref.read(celoProvider).createCeloDID(data, context);
},
The onPressed
properties of the button on the screen holds the series of event that happens when the user clicks the button. It firstly checks if the user fills the fields and shows an error snackbar if the fields are not completely filled. Then it creates a variable of type CeloDIDModel
which assigns the required values to its properties.
Finally, the createCeloDID
function of the controller class is called which is shown below:
Future<dynamic> createCeloDID(CeloDIDModel data, dynamic context) async {
try {
String did = addCeloPrefix(data.identifier ?? '');
createStatus =
createStatus != Status.loading ? Status.loading : Status.done;
notifyListeners();
if (createStatus == Status.done) return;
String? response =
await helper.registerDID(data.address!, did, data.identifier!);
if (response != null) {
print('RESPONSE =====>>>>> $response');
createStatus = Status.done;
notifyListeners();
await storage.setCreatedBoolean();
await storage.setIdentifier(data.identifier.toString());
await storage.setUserAddress(data.address!);
alertDialogs(
context,
'Celo DID Created',
'Celo DID successfuly registered. You can now authenticate with your identifier by using the address $response',
() => Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute<dynamic>(
builder: (dynamic context) => const HomePage()),
(route) => false));
} else {
createStatus = Status.done;
CustomSnackbar.responseSnackbar(
context, Colors.redAccent, 'unable to register did');
}
notifyListeners();
} catch (e) {
createStatus = Status.done;
notifyListeners();
CustomSnackbar.responseSnackbar(context, Colors.redAccent, e.toString());
debugPrint(e.toString());
}
}
- The
addCeloPrefix
function called is a function that adds the celo unique did to the identifier entered by the user. It accepts the identifier string, add the celo did prefix and returns the final string.
String addCeloPrefix(String identifier) {
String prefix = 'did:celo:';
String did = prefix + identifier.trim().toLowerCase();
return did;
}
- From the
createCeloDID
function above, the function then calls theregisterDID
function of theCelo Web3
class situated in theSERVICES
folder. Once the user is successfully registered, the did returned will then be displayed to the user through the alert dialog on the screen.
Future<dynamic> registerDID(
String address, String did, String identifier) async {
final DeployedContract contract = await deployedCeloContract;
final EthereumAddress ethAddress = EthereumAddress.fromHex(address);
final ContractFunction registerDIDFunction =
contract.function('registerDID');
final response = await client.sendTransaction(
credentials,
Transaction.callContract(
contract: contract,
function: registerDIDFunction,
parameters: <dynamic>[ethAddress, did, identifier],
),
chainId: 44787,
);
final dynamic receipt = await awaitResponse(response);
return receipt;
}
- The
registerDID
function of theCeloWeb3Helper
class sends a transaction to the deployed contract on the celo blockchain passing the required parameters and then returns a transaction receipt showing a successful transaction.
Below are the basic contents of the CeloWeb3Helper class file which also contains the other methods/functions like verifyDID
, fetchDID
, changeDID
, and authenticateUser
.
import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
final Web3Client client =
Web3Client('https://alfajores-forno.celo-testnet.org', Client());
String? privateKey = dotenv.env['PRIVATE_KEY'];
final String? contractAddress = dotenv.env['CONTRACT_ADDRESS'];
// ensure the PRIVATE_KEY and CONTRACT_ADDRESS variables are created inside the .env file in your project folder
class CeloWeb3Helper {
/// Get deployed CeloDIDRegistry contract
Future<DeployedContract> get deployedCeloContract async {
const String abiDirectory = 'lib/module/services/did.abi.json';
String contractABI = await rootBundle.loadString(abiDirectory);
final DeployedContract contract = DeployedContract(
ContractAbi.fromJson(contractABI, 'CeloDIDRegistry'),
EthereumAddress.fromHex(contractAddress!),
);
return contract;
}
final EthPrivateKey credentials = EthPrivateKey.fromHex(privateKey ?? '');
Future<dynamic> registerDID(
String address, String did, String identifier) async {
final DeployedContract contract = await deployedCeloContract;
final EthereumAddress ethAddress = EthereumAddress.fromHex(address);
final ContractFunction registerDIDFunction =
contract.function('registerDID');
final response = await client.sendTransaction(
credentials,
Transaction.callContract(
contract: contract,
function: registerDIDFunction,
parameters: <dynamic>[ethAddress, did, identifier],
),
chainId: 44787,
);
final dynamic receipt = await awaitResponse(response);
return receipt;
}
Future<dynamic> fetchDID(String address, String identifier) async {
final DeployedContract contract = await deployedCeloContract;
final EthereumAddress ethAddress = EthereumAddress.fromHex(address);
final fetchDIDFunction = contract.function('getDIDByAddress');
final response = await client.call(
contract: contract,
function: fetchDIDFunction,
params: <dynamic>[ethAddress, identifier],
);
Uint8List bytes = Uint8List.fromList(response[0].toList());
String byte32 = '0x${hex.encode(bytes)}';
return byte32;
}
Future<dynamic> authenticateUser(String address, String celoDid) async {
final DeployedContract contract = await deployedCeloContract;
final EthereumAddress ethAddress = EthereumAddress.fromHex(address);
List<int> bytes = hex.decode(celoDid.replaceAll('0x', ''));
Uint8List did = Uint8List.fromList(bytes);
final authenticateUser = contract.function('authenticateUser');
final response = await client.call(
contract: contract,
function: authenticateUser,
params: <dynamic>[ethAddress, did],
);
print("response ====>> $response");
return response[0];
}
Future<dynamic> changeDID(String address, String oldDid, String newDid,
String identifier, String newIdentifier) async {
// check github repo for the complete code
}
Future<dynamic> verifyDID(String address, String did, String identifier ){
// check the github repo for full code
}
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++;
}
}
-
The
deployedCeloContract
returns the DeployedContract when called inside the function. -
The
registerDID
function as earlier explained, calls the registerDID function of the smart contract and returns the TransactionReceipt.
- The
fetchDID
function accepts the address, and the user’s unique identifier, then converts the address to EthereumAddress which then calls the smart contract functiongetDIDByAddress
. The response is converted to Uint8List and then encoded to bytes. The string interpolation adds 0x to the encoded Uint8List and then returns the string which is the user’s DID.
- The
authenticateUser
function is called when the user is about to make a transaction on the DApp or communicate with any smart contract that requires authentication. It takes the user’s address and did and then confirms the address and did are mapped on the contract then returns true.
This should redirect the user to their wallet for payment or to the smart contract screen for further usage. But for the purpose of this tutorial, we have ensured each user has their Celo decentralized identity and can use the identity to interact with smart contract or other functionality you might want to use it for.
- The
changeDID
function takes the address, the current and new identifiers, the current did and new interpolated did, calls thechangeDID function of the contract
and then changes the did and unique identifier.
- The
verifyDID
function verifies the user by changing theisVerified
property of theIdentity
struct mapped to the user address
- The
awaitResponse
function takes the response variable from each transaction and ensures the transaction is processed and receipt is generated before returning the receipt.
Conclusion
Through this tutorial, we’ve shown how to implement key functionalities such as registering a DID, changing a DID, verifying a DID, and getting a DID by an address. I’ve taken you through each step, from understanding and setting up the smart contract on the Celo network using Celo Composer, to interacting with the contract using the web3dart package in Flutter.
This project exemplifies the potential of integrating blockchain technologies into mobile applications. It opens the door to numerous possibilities, from enhancing user experience with greater data privacy and control to paving the way for more secure transactions.
Next Steps
This tutorial just scratches the surface of what’s possible with Celo’s DID system and Flutter. The possibilities are truly limitless. So, keep exploring, keep learning, and most importantly, keep innovating. I look forward to seeing what you create next!
Also Check out the collection of Flutter tutorials for building decentralized apps on the Celo platform.
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.