Learn to build privacy-preserving identity verification with Self Protocol and bridge it cross-chain using Hyperlane - verify on Celo, use on Base!
πΊ New to Self? Watch the ETHGlobal Workshop first.
This branch demonstrates cross-chain verification bridging. For simple on-chain verification, check the main branch.
main: on chain verificationbackend-verification: off chain/backend verificationhyperlane-example: onchain verification w/ Hyperlane bridging
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CELO SEPOLIA (Source Chain) β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β ProofOfHumanSender β β
β β - Inherits SelfVerificationRoot β β
β β - Verifies users via Self Protocol β β
β β - Automatic cross-chain bridging β β
β ββββββββββββββββββ¬ββββββββββββββββββββββ β
β β β
β β Hyperlane Message β
β βΌ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββ
β BASE SEPOLIA (Destination Chain) β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββ β
β β ProofOfHumanReceiver β β
β β - Receives Hyperlane messages β β
β β - Stores verification data β β
β β - Simple & gas-efficient β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Node.js 20+
- Self Mobile App
- Celo Sepolia wallet with testnet funds
- Base Sepolia wallet with testnet funds (for receiver deployment)
# Clone the boilerplate repository
git clone https://github.com/selfxyz/self-integration-boilerplate.git
git switch hyperlane-example
cd workshop
# Install frontend dependencies
cd app
npm install
# Install contract dependencies
cd contracts
npm install
forge install foundry-rs/forge-stdFirst, deploy the receiver contract on Base Sepolia:
# Create copy of env
cp .env.example .envEdit .env with your Base Sepolia wallet:
# Your private key (with 0x prefix)
PRIVATE_KEY=0xyour_private_key_hereDeploy the receiver:
# Make script executable
chmod +x script/deploy-proof-of-human-receiver.sh
# Deploy receiver on Base Sepolia
./script/deploy-proof-of-human-receiver.shThe script will:
- β Deploy the receiver contract on Base Sepolia
- β Display the contract address
- β
Provide instructions to update your
.envfiles
Save the RECEIVER_ADDRESS for the next step!
Add the receiver address to your .env:
# Add this line to .env
RECEIVER_ADDRESS=0x... # Address from Step 2
# Optional: Customize scope seed
SCOPE_SEED="proof-of-human-hyperlane"Deploy the sender:
# Make script executable
chmod +x script/deploy-proof-of-human-sender.sh
# Deploy contract
./script/deploy-proof-of-human.shThe script will:
- β Deploy the sender contract on Celo Sepolia
- β Fund the contract with 0.01 CELO (~10 automatic bridges)
- β Display both contract addresses and contract balance
- β
Provide complete frontend
.envconfiguration - β
Show commands to automatically update your frontend
.env
The script provides all the configuration you need and funds automatic bridging!
Navigate to app folder
# Create copy of env
cp .env.example .env
# Edit .env with the values from the deployment script outputOption B: Automatic Configuration (Recommended)
The deployment script shows commands to automatically update your .env:
cd ../app
echo 'NEXT_PUBLIC_SELF_ENDPOINT=0x...' > .env
echo 'NEXT_PUBLIC_RECEIVER_ADDRESS=0x...' >> .env
echo 'NEXT_PUBLIC_SELF_APP_NAME="Self + Hyperlane Workshop"' >> .env
echo 'NEXT_PUBLIC_SELF_SCOPE_SEED="proof-of-human-hyperlane"' >> .envJust copy and paste the commands from the deployment script output!
# Start the Next.js development server
cd app
npm run devVisit http://localhost:3000 to see your verification application!
- User verifies on Celo Sepolia through your frontend
- Verification succeeds β
customVerificationHookis called - Contract uses its balance β Automatically pays Hyperlane gas & bridges to Base
- Data arrives on Base β Receiver stores verification status (~2 minutes)
The deployment script automatically funds the sender contract with 0.01 CELO, enough for ~10 automatic bridges.
If the contract runs out of funds or you need to re-send:
# Fund the contract for more automatic bridges
cast send <SENDER_ADDRESS> --value 0.01ether --rpc-url celo-sepolia --private-key $PRIVATE_KEY
# Or manually bridge a specific verification
export SENDER_ADDRESS=0x...
export RECEIVER_ADDRESS=0x...
forge script script/SendVerificationCrossChain.s.sol:SendVerificationCrossChain \
--rpc-url celo-sepolia \
--broadcastThe Self SDK is configured in your React components (app/app/page.tsx):
import { SelfAppBuilder, countries } from '@selfxyz/qrcode';
const app = new SelfAppBuilder({
version: 2, // Always use V2
appName: process.env.NEXT_PUBLIC_SELF_APP_NAME,
scope: process.env.NEXT_PUBLIC_SELF_SCOPE_SEED,
endpoint: process.env.NEXT_PUBLIC_SELF_ENDPOINT, // Your contract address (must be lowercase)
logoBase64: "https://i.postimg.cc/mrmVf9hm/self.png", // Logo URL or base64
userId: userId, // User's wallet address or identifier
endpointType: "staging_celo", // "staging_celo" for testnet, "celo" for mainnet
userIdType: "hex", // "hex" for Ethereum addresses, "uuid" for UUIDs
userDefinedData: "Hola Buenos Aires!!!", // Optional custom data
disclosures: {
minimumAge: 18,
excludedCountries: [countries.UNITED_STATES],
}
}).build();ProofOfHumanSender inherits from SelfVerificationRoot:
contract ProofOfHumanSender is SelfVerificationRoot {
function customVerificationHook(
ISelfVerificationRoot.GenericDiscloseOutputV2 memory output,
bytes memory userData
) internal override {
// Store verification data
verificationSuccessful = true;
lastOutput = output;
lastUserAddress = address(uint160(output.userIdentifier));
emit VerificationCompleted(output, userData);
// Automatically bridge if ETH was sent with verification
if (address(this).balance > 0) {
bytes memory message = abi.encode(
bytes32(output.userIdentifier),
lastUserAddress,
userData,
block.timestamp
);
bytes32 messageId = MAILBOX.dispatch{value: address(this).balance}(
DESTINATION_DOMAIN,
defaultRecipient.addressToBytes32(),
message
);
emit VerificationSentCrossChain(messageId, defaultRecipient, lastUserAddress, bytes32(output.userIdentifier));
}
}
}ProofOfHumanReceiver is a simple Hyperlane message receiver:
contract ProofOfHumanReceiver is IMessageRecipient, Ownable {
function handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) external override {
// Verify caller is Hyperlane Mailbox
if (msg.sender != address(MAILBOX)) revert NotMailbox();
// Verify origin is Celo Sepolia
if (_origin != SOURCE_DOMAIN) revert InvalidOrigin(_origin, SOURCE_DOMAIN);
// Decode and store verification data
(bytes32 userIdentifier, address userAddress, bytes memory userData, uint256 verifiedAt)
= abi.decode(_message, (bytes32, address, bytes, uint256));
verifications[userAddress] = VerificationData({
userIdentifier: userIdentifier,
userAddress: userAddress,
userData: userData,
verifiedAt: verifiedAt,
receivedAt: block.timestamp,
exists: true,
isVerified: true
});
emit VerificationReceived(userAddress, userIdentifier, block.timestamp);
}
}- Chain ID: 11142220
- Identity Hub:
0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74 - Hyperlane Mailbox:
0xD0680F80F4f947968206806C2598Cbc5b6FE5b03 - RPC:
https://forno.celo-sepolia.celo-testnet.org - Explorer:
https://celo-sepolia.blockscout.com/
- Chain ID: 84532
- Hyperlane Mailbox:
0x6966b0E55883d49BFB24539356a2f8A673E02039 - RPC:
https://sepolia.base.org - Explorer:
https://sepolia.basescan.org
cd contracts
forge test -vvAll 23 tests should pass:
ProofOfHumanSender: 8 testsProofOfHumanReceiver: 11 testsHyperlaneCrossChain: 3 tests
After a user verifies, check the message on Hyperlane Explorer:
# Get the message ID from transaction logs
cast logs --rpc-url celo-sepolia \
--address <SENDER_ADDRESS> \
--from-block <BLOCK_NUMBER>
# Track on Hyperlane Explorer
https://explorer.hyperlane.xyz/message/<MESSAGE_ID># Check if user is verified on Base Sepolia
cast call <RECEIVER_ADDRESS> \
"isVerified(address)(bool)" \
<USER_ADDRESS> \
--rpc-url base-sepoliaself-integration-example/
βββ app/ # Next.js frontend application
β βββ app/
β β βββ page.tsx # QR code verification page
β β βββ verified/ # Success page
β β βββ globals.css
β βββ .env.example
β
βββ contracts/ # Foundry contracts
β βββ src/
β β βββ ProofOfHumanSender.sol # Sender contract (Celo)
β β βββ ProofOfHumanReceiver.sol # Receiver contract (Base)
β β βββ ProofOfHuman.sol # Base implementation
β β βββ IMailboxV3.sol # Hyperlane interface
β β
β βββ script/
β β βββ Base.s.sol # Base script utilities
β β βββ DeployProofOfHuman.s.sol # Foundry deployment script
β β βββ deploy-proof-of-human.sh # Automated deployment script
β βββ lib/ # Dependencies
β β βββ forge-std/ # Foundry standard library
β β βββ openzeppelin-contracts/ # OpenZeppelin contracts
β βββ .env.example # Contract environment template
β βββ foundry.toml # Foundry configuration
β βββ package.json # Contract dependencies
β β βββ deploy-proof-of-human-receiver.sh # Deploy receiver
β β βββ deploy-proof-of-human-sender.sh # Deploy sender
β β βββ DeployProofOfHumanReceiver.s.sol
β β βββ DeployProofOfHumanSender.s.sol
β β βββ SendVerificationCrossChain.s.sol # Manual bridging
β β
β βββ test/
β βββ ProofOfHumanSender.t.sol
β βββ ProofOfHumanReceiver.t.sol
β βββ HyperlaneCrossChain.t.sol
β
βββ README.md # This file
- Self Protocol Docs - Identity verification
- Hyperlane Docs - Cross-chain messaging
- Contract Integration Guide
- Sender (Celo Sepolia):
0x210cEb7F310197a3D4E83554086cCeD570314Ee4 - Receiver (Base Sepolia):
0x0690e42FA30BcC48Dd0bf8BF926654e6efDFee89
- Self on iOS - iOS App
- Self on Android - Android App