Introduction
This tutorial will show you how configure Socialconnect SDK, Register and deregister user phone number through Socialconnect SDK and seend payment to user phone number (Involve resolving phone number to wallet address using Socialconnect SDK).
Prerequisites
The assumption made in this tutorial is that you are already familiar with solidity and are aware of how smart contracts operate. It will be expected that you are familiar with how to use your preferred web3 tooling to deploy smart contracts to a network (Hardhat, Truffle, etc). Due to Celo’s compatibility with the Ethereum Virtual Machine (EVM), all the languages and workflows you learnt when developing for other EVM chains also apply to Celo.
- Basic knowledge of JavaScript and ReactJS.
- Understanding of the basics of blockchain and Ethereum.
- Knowledge of Solidity, the programming language for writing smart contracts on CELO.
- A wallet for interacting with the CELO network, such as Celo Wallet Extension, Valora etc.
- Understanding of concepts like smart contract deployment, transactions, gas, and Web3 providers.
Getting Started
To bootstrap our React DAPP, we will be using Celo Composer.
Celo Composer
Celo Composer allows you to quickly build, deploy, and iterate on decentralized applications using Celo. It provides several frameworks, examples, and Celo-specific functionality to help you get started with your next dApp.
You can check out Celo Composer here https://github.com/celo-org/celo-composer/tree/main/packages/react-app.
To start our React App Project, we will use the Celo Composer CLI; the CLI makes it easy for us to select the options that we want when bootstrapping our dApp. Run this on your terminal
npx @celo/celo-composer@latest create
-
If you don’t have celo-compoer installed press enter to continue
-
Choose React when asked for the framework
-
Then choose React Celo for the web3 library
Choose hardhat (Only Hardhat is available at the time of writing this tutorial)
- Skip subgraph, we won’t use it in this tutorial.
Your Project will now be created; you can check to make sure it has the following folders
- packages/hardhat - Your Hardhat Folder - Where you can keep your Contracts
- packages/react-app - Your React project
Install additional dependencies.
In order to configure Socialconnect SDK, we need to install some dependencies in our react app project.
yarn add @tailwindcss/forms @tailwindcss/typography bignumber.js @next/font react-hot-toast react-modal @celo/identity blind-threshold-bls@https://github.com/celo-org/blind-threshold-bls-wasm#3d1013a --exact
Copy blind_threshold_bls_bg.wasm
into the /public
folder of your web project, so that it’s accessible via an absolute path. Ensure that its location matches the path specified in the init
function in the WebBlsBlindingClient
that is used.
Source: Socialconnect SDK
Set up your issuer (read “Authentication” section in privacy.md), which is the account registering attestations. When a user requests for the issuer to register an attestation, the issuer should verify somehow that the user owns their identifier (ex. SMS verification for phone number identifiers).
Manage Environmental Variables
Create .env file to manage project environmental variables and update the file with the following variables
NEXT_PUBLIC_ISSUER_PRIVATE_KEY=
NEXT_PUBLIC_ISSUER_ADDRESS=
NEXT_PUBLIC_TWILIO_URL=
NEXT_PUBLIC_DEK_PUBLIC_KEY=
NEXT_PUBLIC_DEK_PRIVATE_KEY=
In order to send sms to user for verification we will be using twilio. Follow this link to setup twilio.
Manage Twilio Backend
Let’s create a file to structure all necessary function related to twilio like send-sms, verify otp etc. At the root of our react app folder, create a folder named services
then create a file twilio.ts
and pasted the below code snippet in it.
export const E164_REGEX = /^\+[1-9][0-9]{1,14}$/;
export function validatePhoneNumber(phoneNumber:string):boolean {
if (E164_REGEX.test(phoneNumber)) {
return true
}
return false
}
export async function sendSmsVerificationToken(phoneNumber: string) {
try {
if (!validatePhoneNumber(phoneNumber)) {
throw "Attempting to hash a non-e164 number: " + phoneNumber;
}
const data = JSON.stringify({
to: phoneNumber,
channel: "sms",
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_TWILIO_URL}/start-verify`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: data,
}
);
console.log("Verification request response:", response);
} catch (error) {
throw `Failed SMS verification: ${error}`;
}
}
export async function verifyToken(
phoneNumber: string,
receivedCode: string
): Promise<boolean> {
try {
const data = JSON.stringify({
to: phoneNumber,
code: receivedCode,
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_TWILIO_URL}/check-verify`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: data,
}
);
const json = await response.json();
console.log("verification response", json.success);
return json.success;
} catch (error) {
console.error(error);
return false;
}
}
Manage Components
In components folder, we need to create a reusable component for toast, let’s create a file toast.ts
inside the components folder update the file with the code below
import toast from 'react-hot-toast';
type ToastType = 'error' | 'warning' | 'success';
const ToastComponent = ({
type,
title,
description,
onDismiss,
}: {
type: ToastType;
title: string;
description?: string;
onDismiss: () => void;
}) => (
<div className="w-full py-3 px-1">
<div className="flex items-start">
<div className="flex-shrink-0">
{type === 'warning' && (
<svg
className="h-6 w-6 text-yellow-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
{type === 'error' && (
<svg
className="h-6 w-6 text-red-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
{type === 'success' && (
<svg
className="h-6 w-6 text-green-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
</div>
<div className="ml-3 w-0 flex-1 pt-0.5">
<p className="text-sm font-medium text-gray-800">{title}</p>
{description && (
<p className="mt-1 text-sm text-gray-600">{description}</p>
)}
</div>
<div className="ml-4 flex-shrink-0 flex">
<button
onClick={onDismiss}
className="rounded-md inline-flex text-gray-700 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span className="sr-only">Close</span>
{/* <!-- Heroicon name: x --> */}
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
);
const createToast = (type: ToastType, title: string, description?: string) =>
toast(
(t) => (
<ToastComponent
type={type}
title={title}
description={description}
onDismiss={() => toast.dismiss(t.id)}
/>
),
{
duration: 5000,
}
);
export const error = (message: string, description?: string): string =>
createToast('error', message, description);
export const warning = (message: string, description?: string): string =>
createToast('warning', message, description);
export const success = (message: string, description?: string): string =>
createToast('success', message, description);
Then lastly, we need a file to manage bls client, create a file name bls-blinding-client.ts
import { randomBytes } from 'crypto'
import thresholdBls from 'blind-threshold-bls'
export interface BlsBlindingClient {
blindMessage: (base64PhoneNumber: string, seed?: Buffer) => Promise<string>
unblindAndVerifyMessage: (blindedMessage: string) => Promise<string>
}
// The following interfaces should match https://github.com/celo-org/blind-threshold-bls-wasm/blob/master/src/blind_threshold_bls.d.ts
interface ThresholdBlsLib {
blind: (message: Uint8Array, seed: Uint8Array) => BlindedMessage
unblind: (blindedSignature: Uint8Array, blindingFactor: Uint8Array) => Uint8Array
verify: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => void // throws on failure
}
interface BlindedMessage {
blindingFactor: Uint8Array
message: Uint8Array
}
export default class WebBlsBlindingClient implements BlsBlindingClient {
private odisPubKey: Uint8Array
private blindedValue: BlindedMessage | undefined
private rawMessage: Buffer | undefined
constructor(odisPubKey: string) {
this.odisPubKey = Buffer.from(odisPubKey, 'base64')
}
async init() {
await thresholdBls.init("/blind_threshold_bls_bg.wasm")
}
async blindMessage(base64PhoneNumber: string, seed?: Buffer): Promise<string> {
const userSeed = seed ?? randomBytes(32)
if (!seed) {
console.warn(
'Warning: Use a private deterministic seed (e.g. DEK private key) to preserve user quota when requests are replayed.'
)
}
this.rawMessage = Buffer.from(base64PhoneNumber, 'base64')
this.blindedValue = await thresholdBls.blind(this.rawMessage, userSeed)
const blindedMessage = this.blindedValue.message
return Buffer.from(blindedMessage).toString('base64')
}
async unblindAndVerifyMessage(base64BlindSig: string): Promise<string> {
if (!this.rawMessage || !this.blindedValue) {
throw new Error('Must call blind before unblinding')
}
const blindedSignature = Buffer.from(base64BlindSig, 'base64')
const unblindMessage = await thresholdBls.unblind(
blindedSignature,
this.blindedValue.blindingFactor
)
// this throws on error
await thresholdBls.verify(this.odisPubKey, this.rawMessage, unblindMessage)
return Buffer.from(unblindMessage).toString('base64')
}
private isReactNativeEnvironment(): boolean {
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
}
}
For user to be able to register/link their phone number with wallet address, let’s create a modal where user can input and verify their phone number. Create new file in components, name it RegisterNumberModal.tsx
import Modal from "react-modal";
import { useState } from "react";
import {
sendSmsVerificationToken,
validatePhoneNumber,
verifyToken,
} from "../services/twilio";
export default function RegisterNumberModal({
isOpen,
onDismiss,
registerNumber,
}: {
isOpen: boolean;
onDismiss: () => void;
registerNumber: (number: string) => Promise<void>;
}) {
const [activeIndex, setActiveIndex] = useState(0);
const [number, setNumber] = useState("");
const [userCode, setUserCode] = useState("");
const [invalidInput, setInvalidInput] = useState(false);
const [doneLoading, setDoneLoading] = useState(false);
function editNumber(input: string) {
setInvalidInput(false);
setNumber(input);
}
function editCode(input: string) {
setInvalidInput(false);
setUserCode(input);
}
async function sendVerificationText() {
if (!validatePhoneNumber(number)) {
setInvalidInput(true);
return;
}
await sendSmsVerificationToken(number);
setInvalidInput(false);
setActiveIndex(1);
}
async function validateCode() {
const successfulVerification = await verifyToken(number, userCode);
if (successfulVerification) {
setActiveIndex(2);
await registerNumber(number);
setDoneLoading(true);
} else {
setInvalidInput(true);
}
}
function closeModal() {
setActiveIndex(0);
setNumber("");
setUserCode("");
setDoneLoading(false);
setInvalidInput(false);
onDismiss();
}
const customStyles = {
content: {
margin: "10%",
borderRadius: "0px",
padding: "0",
boxShadow: "0",
background: "#FCF6F1",
border: '1px solid #CCCCCC',
height: "fit-content",
},
};
return (
<Modal isOpen={isOpen} style={customStyles}>
{activeIndex === 0 ? (
<div className="">
<div className="p-20">
<h2 className="py-5">Verify your phone number</h2>
<label
htmlFor="numberToRegister"
className="block text-sm font-medium text-gray-700"
>
Phone number
</label>
<input
type="text"
name="numberToRegister"
id="numberToRegister"
value={number}
onChange={(e) => editNumber(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
/>
{invalidInput && (
<small>
Not a valid phone number! Make sure you include the country code
</small>
)}
</div>
<div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
<button
className="inline-flex items-center rounded-full border border-wood bg-prosperity py-2 px-10 text-md font-medium text-black hover:bg-snow"
onClick={sendVerificationText}
>
Verify
</button>
</div>
</div>
) : activeIndex === 1 ? (
<div className="">
<div className="p-20">
<h2 className="py-5">Enter the code we sent to your number</h2>
<label
htmlFor="userCode"
className="block text-sm font-medium text-gray-700"
>
Verification Code
</label>
<input
type="text"
name="userCode"
id="userCode"
value={userCode}
onChange={(e) => editCode(e.target.value)}
className="mt-1 block w-full border-black focus:border-black sm:text-sm"
/>
{invalidInput && (
<small>
Incorrect code! Make sure you're entering the latest code
received to your phone
</small>
)}
</div>
<div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
<button
className="mr-3 inline-flex object-bottom justify-center border py-2 px-4 text-sm font-medium text-white shadow-sm focus:outline-none"
onClick={validateCode}
>
Validate Code
</button>
</div>
</div>
) : activeIndex === 2 ? (
<div className="flex flex-col items-center">
<h2 className="py-5">Registering your phone number</h2>
{!doneLoading ? (
<svg
aria-hidden="true"
className="mr-2 my-10 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600 self-center"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
) : (
<p className="my-10">Done!</p>
)}
</div>
) : null}
<button
className="mt-5 mr-3 absolute top-2 right-3 py-2 px-4 text-md font-medium text-black"
onClick={closeModal}
>
Close
</button>
</Modal>
);
}
Also let’s do same thing for de-register, create a file name DeregisterNumberModal.tsx
import Modal from "react-modal";
import { useState } from "react";
import { validatePhoneNumber } from "../services/twilio";
export default function DeregisterNumberModal({
isOpen,
onDismiss,
deregisterNumber,
}: {
isOpen: boolean;
onDismiss: () => void;
deregisterNumber: (number: string) => Promise<void>;
}) {
const [activeIndex, setActiveIndex] = useState(0);
const [number, setNumber] = useState("");
const [invalidInput, setInvalidInput] = useState(false);
const [doneLoading, setDoneLoading] = useState(false);
function editNumber(input: string) {
setInvalidInput(false);
setNumber(input);
}
async function deregister() {
if (!validatePhoneNumber(number)) {
setInvalidInput(true);
return;
}
// TODO: check that phone number has actually been linked to user's account
setInvalidInput(false);
setActiveIndex(1);
await deregisterNumber(number);
setDoneLoading(true);
}
function closeModal() {
setActiveIndex(0);
setNumber("");
setDoneLoading(false);
setInvalidInput(false);
onDismiss();
}
const customStyles = {
content: {
margin: "10%",
borderRadius: "0px",
padding: "0",
boxShadow: "0",
background: "#FCF6F1",
border: '1px solid #CCCCCC',
height: "fit-content",
},
};
return (
<Modal isOpen={isOpen} style={customStyles}>
{activeIndex === 0 ? (
<div className="">
<div className="p-20">
<h2 className="py-5">De-register a phone number</h2>
<label
htmlFor="number"
className="block text-sm font-medium text-gray-700"
>
Phone number
</label>
<input
type="text"
name="number"
id="number"
value={number}
onChange={(e) => editNumber(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
/>
{invalidInput && (
<small>
Not a valid phone number! Make sure you include the country code
</small>
)}
</div>
<div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
<button
className="inline-flex items-center rounded-full border border-wood bg-prosperity py-2 px-10 text-md font-medium text-black hover:bg-snow"
onClick={deregister}
>
De-register
</button>
</div>
</div>
) : activeIndex === 1 ? (
<div className="flex flex-col items-center">
<h2 className="py-5">De-registering {number}</h2>
{!doneLoading ? (
<svg
aria-hidden="true"
className="mr-2 my-10 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600 self-center"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
) : (
<p className="my-10">Done!</p>
)}
</div>
) : null}
<button
className="mt-5 mr-3 absolute top-2 right-3 py-2 px-4 text-md font-medium text-black"
onClick={closeModal}
>
Close
</button>
</Modal>
);
}
Then a payment modal that allow user to input other user phone number in order to send payment to them, create file name SendToNumberModal.tsx
import Modal from "react-modal";
import { useState } from "react";
import { validatePhoneNumber } from "../services/twilio";
import BigNumber from "bignumber.js";
export default function SendToNumberModal({
isOpen,
onDismiss,
sendToNumber,
}: {
isOpen: boolean;
onDismiss: () => void;
sendToNumber: (number: string, amount: string) => Promise<void>;
}) {
const [activeIndex, setActiveIndex] = useState(0);
const [number, setNumber] = useState("");
const [sendAmount, setSendAmount] = useState("");
const [invalidInput, setInvalidInput] = useState(false);
const [doneLoading, setDoneLoading] = useState(false);
function editNumber(input: string) {
setInvalidInput(false);
setNumber(input);
}
function editAmount(input: string) {
setInvalidInput(false);
setSendAmount(input);
}
async function checkPhoneNumber() {
if (!validatePhoneNumber(number)) {
setInvalidInput(true);
return;
}
// TODO: choose from linked addresses
setInvalidInput(false);
setActiveIndex(1);
}
async function send() {
if (!new BigNumber(sendAmount).isGreaterThan(0)) {
setInvalidInput(true);
return;
}
setActiveIndex(2);
await sendToNumber(number, sendAmount);
setDoneLoading(true);
}
function closeModal() {
setActiveIndex(0);
setNumber("");
setSendAmount("");
setDoneLoading(false);
setInvalidInput(false);
onDismiss();
}
const customStyles = {
content: {
margin: "10%",
borderRadius: "0px",
padding: "0",
boxShadow: "0",
background: "#FCF6F1",
border: '1px solid #CCCCCC',
height: "fit-content",
},
};
return (
<Modal isOpen={isOpen} style={customStyles}>
{activeIndex === 0 ? (
<div className="">
<div className="p-20">
<h2 className="py-5">Recipient phone number</h2>
<label
htmlFor="number"
className="block text-sm font-medium text-gray-700"
>
Phone number
</label>
<input
type="text"
name="number"
id="number"
value={number}
onChange={(e) => editNumber(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
/>
{invalidInput && (
<small>
Not a valid phone number! Make sure you include the country code
</small>
)}
</div>
<div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
<button
className="inline-flex items-center rounded-full border border-wood bg-prosperity py-2 px-10 text-md font-medium text-black hover:bg-snow"
onClick={checkPhoneNumber}
>
Send
</button>
</div>
</div>
) : activeIndex === 1 ? (
<div className="">
<div className="p-20">
<h2 className="py-5">Amount to send</h2>
<label
htmlFor="amount"
className="block text-sm font-medium text-gray-700"
>
CELO
</label>
<input
type="text"
name="amount"
id="amount"
value={sendAmount}
onChange={(e) => editAmount(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-celo-green focus:ring-celo-green sm:text-sm"
/>
{invalidInput && (
<small>Invalid amount! Needs to be a positive amount</small>
)}
</div>
<div className="object-bottom bg-gray-50 px-4 py-3 text-right sm:px-6 ">
<button
className="mr-3 inline-flex object-bottom justify-center rounded-md border border-transparent bg-celo-green py-2 px-4 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-celo-green focus:ring-offset-2"
onClick={send}
>
Send
</button>
</div>
</div>
) : activeIndex === 2 ? (
<div className="flex flex-col items-center">
<h2 className="py-5">
Sending {sendAmount} CELO to {number}
</h2>
{!doneLoading ? (
<svg
aria-hidden="true"
className="mr-2 my-10 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600 self-center"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
) : (
<p className="my-10">Done!</p>
)}
</div>
) : null}
<button
className="mt-5 mr-3 absolute top-2 right-3 py-2 px-4 text-md font-medium text-black"
onClick={closeModal}
>
Close
</button>
</Modal>
);
}
Manage Page
Let’s update the index page with code below, open pages in the root folder of our react app then update the ```index.tsx`` file with the code below
import { ContractKit, newKit } from "@celo/contractkit";
import { BigNumber } from "bignumber.js";
import { useEffect, useState } from "react";
import { toast } from "../components";
import { OdisUtils } from "@celo/identity";
import WebBlsBlindingClient from "../components/bls-blinding-client";
import { useCelo } from "@celo/react-celo";
import "@celo/react-celo/lib/styles.css";
import { E164_REGEX } from "../services/twilio";
import { Account } from "web3-core";
import {
AuthSigner,
getServiceContext,
OdisContextName,
} from "@celo/identity/lib/odis/query";
import { FederatedAttestationsWrapper } from "@celo/contractkit/lib/wrappers/FederatedAttestations";
import { OdisPaymentsWrapper } from "@celo/contractkit/lib/wrappers/OdisPayments";
import RegisterNumberModal from "./components/RegisterNumberModal";
import SendToNumberModal from "./components/SendToNumberModal";
import DeregisterNumberModal from "./components/DeregisterNumberModal";
import { IdentifierPrefix } from "@celo/identity/lib/odis/identifier";
function App() {
let [componentInitialized, setComponentInitialized] = useState(false);
const { initialised, kit, connect, address, destroy, network } = useCelo();
const ISSUER_PRIVATE_KEY = process.env.NEXT_PUBLIC_ISSUER_PRIVATE_KEY;
const DEK_PRIVATE_KEY = process.env.NEXT_PUBLIC_DEK_PRIVATE_KEY;
let issuerKit: ContractKit,
issuer: Account,
federatedAttestationsContract: FederatedAttestationsWrapper,
odisPaymentContract: OdisPaymentsWrapper;
const [isRegisterNumberModalOpen, setIsRegisterNumberModalOpen] =
useState(false);
const [isSendToNumberModalOpen, setIsSendToNumberModalOpen] = useState(false);
const [isDeregisterNumberModalOpen, setIsDeregisterNumberModalOpen] =
useState(false);
useEffect(() => {
if (initialised) {
setComponentInitialized(true);
}
}, [initialised]);
useEffect(() => {
const intializeIssuer = async () => {
issuerKit = newKit(network.rpcUrl);
issuer =
issuerKit.web3.eth.accounts.privateKeyToAccount(ISSUER_PRIVATE_KEY);
issuerKit.addAccount(ISSUER_PRIVATE_KEY);
issuerKit.defaultAccount = issuer.address;
federatedAttestationsContract =
await issuerKit.contracts.getFederatedAttestations();
odisPaymentContract = await issuerKit.contracts.getOdisPayments();
};
intializeIssuer();
});
async function getIdentifier(phoneNumber: string) {
try {
if (!E164_REGEX.test(phoneNumber)) {
throw "Attempting to hash a non-e164 number: " + phoneNumber;
}
const ONE_CENT_CUSD = issuerKit.web3.utils.toWei("0.01", "ether");
let authMethod: any = OdisUtils.Query.AuthenticationMethod.ENCRYPTION_KEY;
const authSigner: AuthSigner = {
authenticationMethod: authMethod,
rawKey: DEK_PRIVATE_KEY,
};
const serviceContext = getServiceContext(OdisContextName.ALFAJORES);
//check remaining quota
const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
issuer.address,
authSigner,
serviceContext
);
//increase quota if needed.
console.log("remaining ODIS quota", remainingQuota);
if (remainingQuota < 1) {
// give odis payment contract permission to use cUSD
const cusd = await issuerKit.contracts.getStableToken();
const currrentAllowance = await cusd.allowance(
issuer.address,
odisPaymentContract.address
);
console.log("current allowance:", currrentAllowance.toString());
let enoughAllowance: boolean = false;
if (currrentAllowance < BigNumber(ONE_CENT_CUSD)) {
const approvalTxReceipt = await cusd
.increaseAllowance(odisPaymentContract.address, ONE_CENT_CUSD)
.sendAndWaitForReceipt();
console.log("approval status", approvalTxReceipt.status);
enoughAllowance = approvalTxReceipt.status;
} else {
enoughAllowance = true;
}
// increase quota
if (enoughAllowance) {
const odisPayment = await odisPaymentContract
.payInCUSD(issuer.address, ONE_CENT_CUSD)
.sendAndWaitForReceipt();
console.log("odis payment tx status:", odisPayment.status);
console.log("odis payment tx hash:", odisPayment.transactionHash);
} else {
throw "cUSD approval failed";
}
}
const blindingClient = new WebBlsBlindingClient(
serviceContext.odisPubKey
);
await blindingClient.init();
console.log("fetching identifier for:", phoneNumber);
const response = await OdisUtils.Identifier.getObfuscatedIdentifier(
phoneNumber,
IdentifierPrefix.PHONE_NUMBER,
issuer.address,
authSigner,
serviceContext,
undefined,
undefined,
blindingClient
);
console.log(`Obfuscated phone number: ${response.obfuscatedIdentifier}`);
console.log(
`Obfuscated phone number is a result of: sha3('tel://${response.plaintextIdentifier}__${response.pepper}') => ${response.obfuscatedIdentifier}`
);
return response.obfuscatedIdentifier;
} catch (error) {
throw `failed to get identifier: ${error}`;
}
}
// this function needs to be called once when using a new issuer address for the first time
async function registerIssuerAccountAndDEK() {
if (issuer.address == undefined) {
throw "issuer not found";
}
const accountsContract = await issuerKit.contracts.getAccounts();
// register account if needed
let registeredAccount = await accountsContract.isAccount(issuer.address);
if (!registeredAccount) {
console.log("Registering account");
const receipt = await accountsContract
.createAccount()
.sendAndWaitForReceipt({ from: issuer.address });
console.log("Receipt status: ", receipt.status);
} else {
console.log("Account already registered");
}
// register DEK
const DEK_PUBLIC_KEY = process.env.NEXT_PUBLIC_DEK_PUBLIC_KEY;
console.log("registering dek");
await accountsContract
.setAccountDataEncryptionKey(DEK_PUBLIC_KEY)
.sendAndWaitForReceipt({ from: issuer.address });
console.log("dek registered");
}
async function registerNumber(number: string) {
try {
const verificationTime = Math.floor(new Date().getTime() / 1000);
const identifier = await getIdentifier(number);
console.log(identifier);
// TODO: lookup list of issuers per phone number.
// This could be a good example to have for potential issuers to learn about this feature.
const { accounts } =
await federatedAttestationsContract.lookupAttestations(identifier, [
issuer.address,
]);
console.log(accounts);
if (accounts.length == 0) {
const attestationReceipt = await federatedAttestationsContract
.registerAttestationAsIssuer(identifier, address, verificationTime)
.sendAndWaitForReceipt();
console.log("attestation Receipt status:", attestationReceipt.status);
console.log(
`Register Attestation as issuer TX hash: ${network.explorer}/tx/${attestationReceipt.transactionHash}/internal-transactions`
);
} else {
console.log("phone number already registered with this issuer");
}
} catch (error) {
throw `Error registering phone number: ${error}`;
}
}
async function sendToNumber(number: string, amount: string) {
try {
const identifier = await getIdentifier(number);
const amountInWei = issuerKit.web3.utils.toWei(amount, "ether");
const attestations =
await federatedAttestationsContract.lookupAttestations(identifier, [
issuer.address,
]);
// TODO: handle when no accounts mapped to number
const CELO = await kit.contracts.getGoldToken();
await CELO.transfer(
attestations.accounts[0],
amountInWei
).sendAndWaitForReceipt({ gasPrice: 20000000000 });
} catch (error) {
throw `Failed to send funds to ${number}: ${error}`;
}
}
async function deregisterPhoneNumber(phoneNumber: string) {
try {
const identifier = await getIdentifier(phoneNumber);
const receipt = await federatedAttestationsContract
.revokeAttestation(identifier, issuer.address, address)
.sendAndWaitForReceipt();
console.log(
`revoke attestation transaction receipt status: ${receipt.status}`
);
} catch (error) {
throw `Failed to deregister phone number: ${error}`;
}
}
return (
<main>
<br />
{componentInitialized && address ? (
<div className="flex flex-col mx-auto content-center">
<p className="flex flex-col mx-auto content-center">Connected address (user):</p>
<p>{address}</p>
<button
type="button"
className="inline-flex self-center items-center rounded-full border border-wood bg-prosperity py-5 px-10 my-5 text-md font-medium text-black hover:bg-snow"
onClick={destroy}
>Disconnect user</button>
<div className="my-0 sm:mt-0">
<div className="overflow-hidden">
<div className="bg-fig border-x border-t border-lavender px-4 py-5 text-center sm:px-6">
<button
className="mr-3 inline-flex justify-center py-2 px-4 text-sm font-medium text-white hover:text-sand"
onClick={() => setIsRegisterNumberModalOpen(true)}
>
Verify and register your phone number
</button>
</div>
</div>
</div>
<div className="my-0 sm:mt-0">
<div className="overflow-hidden">
<div className="bg-fig border-x border-t border-lavender px-4 py-5 text-center sm:px-6">
<button
className="mr-3 inline-flex justify-center py-2 px-4 text-sm font-medium text-white hover:text-sand"
onClick={() => setIsSendToNumberModalOpen(true)}
>
Send payment to a phone number
</button>
</div>
</div>
</div>
<div className="my-0 sm:mt-0">
<div className="overflow-hidden">
<div className="bg-fig border-x border-t border-b border-lavender px-4 py-5 text-center sm:px-6">
<button
className="mr-3 inline-flex justify-center py-2 px-4 text-sm font-medium text-white hover:text-sand"
onClick={() => setIsDeregisterNumberModalOpen(true)}
>
De-register your phone number
</button>
</div>
</div>
</div>
<RegisterNumberModal
isOpen={isRegisterNumberModalOpen}
onDismiss={() => setIsRegisterNumberModalOpen(false)}
registerNumber={registerNumber}
/>
<SendToNumberModal
isOpen={isSendToNumberModalOpen}
onDismiss={() => setIsSendToNumberModalOpen(false)}
sendToNumber={sendToNumber}
/>
<DeregisterNumberModal
isOpen={isDeregisterNumberModalOpen}
onDismiss={() => setIsDeregisterNumberModalOpen(false)}
deregisterNumber={deregisterPhoneNumber}
/>
</div>
) : (
<button
type="button"
className="inline-flex items-center rounded-full border border-wood bg-prosperity py-5 px-10 text-md font-medium text-black hover:bg-snow"
onClick={() =>
connect().catch((e) => console.log((e as Error).message))
}
>Connect user</button>
)}
</main>
);
}
export default App;
Let’s test our react app, open your terminal and run the command below
yarn dev
Conclusion
Congratulations, you have now learned how to integrate Socialconnect SDK with Celo Composer. Send Token to user using their phone number.
About the Author
I am a Software Engineer, Tech Evangelist (Preaching the gospel of flutter & blockchain) also and Ex-GDSC Leads.