V1 to V2 Migration Guide
This guide helps you migrate from Self Protocol V1 to V2. V2 introduces multi-document support, dynamic configuration, and improved data structures.
Overview of Changes
What’s New in V2
- Multi-document support: E-Passports, EU ID Cards, Aadhaar, and KYC
- Dynamic configuration: Switch configurations without redeployment
- Enhanced data formats: Pre-extracted, human-readable outputs
- User context data: Pass custom data through verification flow
- Improved error handling: Detailed configuration mismatch reporting
Breaking Changes
- Backend SDK constructor requires new parameters
- Configuration methods replaced with interface
- Verification method signature changed
- Frontend requires disclosures object
- Smart contract interfaces updated
Backend Migration
1. Update Dependencies
npm install @selfxyz/core@latest
2. Update Constructor
V1 (Old):
const verifier = new SelfBackendVerifier(
"my-app-scope",
"https://api.example.com/verify",
false // mock mode
);
V2 (New):
import { SelfBackendVerifier, AttestationId, UserIdType, IConfigStorage, AllIds } from '@selfxyz/core';
// Option 1: Use AllIds for all document types (recommended for most cases)
const allowedIds = AllIds;
// Option 2: Define specific allowed document types
// const allowedIds = new Map();
// allowedIds.set(AttestationId.E_PASSPORT, true); // Accept passports
// allowedIds.set(AttestationId.EU_ID_CARD, true); // Accept EU ID cards
// allowedIds.set(AttestationId.AADHAAR, true); // Accept Aadhaar
// allowedIds.set(AttestationId.KYC, true); // Accept KYC
// Implement configuration storage
class ConfigStorage implements IConfigStorage {
async getConfig(configId: string) {
// Return your verification requirements
return {
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'],
ofac: true
};
}
async getActionId(userIdentifier: string, userDefinedData?: string) {
// Return config ID based on your logic
return 'default_config';
}
}
const verifier = new SelfBackendVerifier(
"my-app-scope",
"https://api.example.com/verify",
false, // mock mode
allowedIds, // NEW: allowed document types
new ConfigStorage(), // NEW: config storage
UserIdType.UUID // NEW: user ID type
);
3. Update Configuration
V1 (Old):
// Direct method calls
verifier.setMinimumAge(18);
verifier.excludeCountries('Iran', 'North Korea');
verifier.enablePassportNoOfacCheck();
verifier.enableNameOfacCheck();
verifier.enableDobOfacCheck();
V2 (New):
// Configuration via IConfigStorage implementation
class ConfigStorage implements IConfigStorage {
async getConfig(configId: string) {
// All configuration in one place
return {
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'], // Use ISO 3-letter codes
ofac: true // Single boolean for all OFAC checks
};
}
}
4. Update Verification Method
V1 (Old):
app.post('/api/verify', async (req, res) => {
const { proof, publicSignals } = req.body;
try {
const isValid = await verifier.verify(proof, publicSignals);
res.json({ valid: isValid });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
V2 (New):
app.post('/api/verify', async (req, res) => {
const { attestationId, proof, pubSignals, userContextData } = req.body;
try {
const result = await verifier.verify(
attestationId, // NEW: 1 passport, 2 EU ID, 3 Aadhaar, 4 KYC
proof,
pubSignals,
userContextData // NEW: hex-encoded context data
);
if (result.isValidDetails.isValid) {
res.json({
status: 'success',
result: true,
credentialSubject: result.discloseOutput,
documentType:
attestationId === 1 ? 'passport' :
attestationId === 2 ? 'eu_id_card' :
attestationId === 3 ? 'aadhaar' :
'kyc'
});
} else {
res.status(400).json({
status: 'error',
result: false,
details: result.isValidDetails
});
}
} catch (error) {
if (error.name === 'ConfigMismatchError') {
res.status(400).json({
status: 'error',
message: 'Configuration mismatch',
issues: error.issues
});
} else {
res.status(500).json({ error: error.message });
}
}
});
5. Handle New Response Format
V1 Response:
// Simple boolean
true/false
V2 Response:
{
attestationId: 1, // Document type
isValidDetails: {
isValid: boolean, // Overall result
isOlderThanValid: boolean, // Age check result
isOfacValid: boolean // OFAC check result
},
forbiddenCountriesList: [], // Excluded countries
discloseOutput: { // Pre-extracted data
nationality: "USA",
minimumAge: "21",
name: ["JOHN", "DOE"],
dateOfBirth: "01-01-1990",
issuingState: "USA",
idNumber: "123456789",
gender: "M",
expiryDate: "01-01-2030",
ofac: [true, true, true]
},
userData: {
userIdentifier: "uuid-here",
userDefinedData: "custom-data"
}
}
Frontend Migration
1. Update Dependencies
npm install @selfxyz/qrcode@latest
2. Update QR Code Configuration
V1 (Old):
import { SelfAppBuilder } from '@selfxyz/qrcode';
const selfApp = new SelfAppBuilder({
appName: "My App",
scope: "my-app-scope",
endpoint: "https://api.example.com/verify",
userId: userId,
logoBase64: logo
}).build();
V2 (New):
import { SelfAppBuilder } from '@selfxyz/qrcode';
const selfApp = new SelfAppBuilder({
appName: "My App",
scope: "my-app-scope",
endpoint: "https://api.example.com/verify",
userId: userId,
logoBase64: logo,
version: 2, // NEW: Specify V2
userDefinedData: "custom-data", // NEW: Optional custom data
disclosures: { // NEW: Must match backend config
// Verification rules
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'],
ofac: true,
// Data fields to reveal
name: true,
nationality: true,
dateOfBirth: true
}
}).build();
3. Important: Disclosures Object
The disclosures object in V2 contains both verification rules and data fields:
disclosures: {
// Verification rules (must match backend exactly)
minimumAge: 18, // Age requirement
excludedCountries: ['IRN', 'PRK'], // ISO 3-letter codes
ofac: true, // OFAC checking
// Data fields to reveal
name: true, // Full name
nationality: true, // Nationality
dateOfBirth: true, // Date of birth
issuingState: true, // Issuing country
idNumber: true, // Document number
gender: true, // Gender
expiryDate: true // Expiration date
}
Smart Contract Migration
1. Update Contract Inheritance
V1 (Old):
import { IIdentityVerificationHub } from "@selfxyz/contracts/interfaces/IIdentityVerificationHub.sol";
contract MyContract {
IIdentityVerificationHub public hub;
function verify(/* params */) external {
// Direct hub integration
}
}
V2 (New):
import { SelfVerificationRoot } from "@selfxyz/contracts/abstract/SelfVerificationRoot.sol";
contract MyContract is SelfVerificationRoot {
constructor(
address _hub,
bytes32 _scope
) SelfVerificationRoot(_hub, _scope) {}
// Override to handle verification results
function customVerificationHook(
GenericDiscloseOutputV2 memory output,
bytes memory userData
) internal override {
// Your business logic here
}
// Override to provide configuration ID
function getConfigId(
bytes32 destinationChainId,
bytes32 userIdentifier,
bytes memory userDefinedData
) public view override returns (bytes32) {
// Generate your config ID at https://tools.self.xyz/
// Default config ID: 0x7b6436b0c98f62380866d9432c2af0ee08ce16a171bda6951aecd95ee1307d61
return 0x7b6436b0c98f62380866d9432c2af0ee08ce16a171bda6951aecd95ee1307d61;
}
}
2. Update Hub Addresses
V2 Hub Addresses:
// Celo Mainnet
address constant HUB_V2 = 0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF;
// Celo Sepolia Testnet
address constant HUB_V2_STAGING = 0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74;
3. Handle New Data Structure
V1 Structure:
struct VcAndDiscloseVerificationResult {
uint256 attestationId;
uint256 scope;
uint256 userIdentifier;
uint256 nullifier;
uint256[3] revealedDataPacked; // Packed data
}
V2 Structure:
struct GenericDiscloseOutputV2 {
bytes32 attestationId; // Now bytes32
uint256 userIdentifier;
uint256 nullifier;
string issuingState; // Pre-extracted
string[] name; // Pre-extracted array
string idNumber; // Renamed from passportNumber
string nationality; // Pre-extracted
string dateOfBirth; // Pre-extracted format
string gender; // Pre-extracted
string expiryDate; // Pre-extracted
uint256 minimumAge;
bool[3] ofac; // Bool array
uint256[4] forbiddenCountriesListPacked;
}
Common Migration Issues
1. Configuration Mismatch
Problem: Frontend disclosures don’t match backend configuration
ConfigMismatchError: Configuration mismatch
Solution: Ensure frontend and backend have identical settings:
// Frontend
disclosures: {
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'],
ofac: true
}
// Backend (in getConfig)
return {
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'],
ofac: true
};
2. Missing Attestation ID
Problem: Verification fails with missing attestation ID
Solution: Frontend must send attestation ID:
const requestBody = {
attestationId: 1, // 1 passport, 2 EU ID, 3 Aadhaar, 4 KYC
proof: proof,
pubSignals: pubSignals,
userContextData: userContextData
};
3. Invalid User Context Data
Problem: User context data validation fails
Solution: Ensure proper hex encoding:
// Create user context data (256 bytes total)
const userContextData = '0x' + '0'.repeat(512); // 512 hex chars = 256 bytes
4. Document Type Not Allowed
Problem: “Attestation ID is not allowed” error
Solution: Add document type to allowedIds:
// Option 1: Use AllIds for all document types
const allowedIds = AllIds;
// Option 2: Define specific allowed document types
// const allowedIds = new Map();
// allowedIds.set(AttestationId.E_PASSPORT, true); // Add passport
// allowedIds.set(AttestationId.EU_ID_CARD, true); // Add EU ID card
// allowedIds.set(AttestationId.AADHAAR, true); // Add Aadhaar
// allowedIds.set(AttestationId.KYC, true); // Add KYC
Testing Your Migration
1. Test with Mock Passports
// Use staging/testnet for development
const verifier = new SelfBackendVerifier(
"test-scope",
"https://test.ngrok.app/verify",
true, // Enable mock mode
allowedIds,
configStorage,
UserIdType.UUID
);
// Disable OFAC for mock passports
class ConfigStorage {
async getConfig() {
return {
minimumAge: 18,
ofac: false // Must be false for mock passports
};
}
}
2. Test Multiple Document Types
// Test passport verification
const passportResult = await verifier.verify(
AttestationId.E_PASSPORT, // 1
passportProof,
passportSignals,
userContextData
);
// Test EU ID card verification
const idCardResult = await verifier.verify(
AttestationId.EU_ID_CARD, // 2
idCardProof,
idCardSignals,
userContextData
);
// Test KYC verification
const kycResult = await verifier.verify(
AttestationId.KYC, // 4
kycProof,
kycSignals,
userContextData
);
3. Verify Configuration Switching
class DynamicConfigStorage {
async getConfig(configId: string) {
switch(configId) {
case 'strict':
return { minimumAge: 21, ofac: true };
case 'relaxed':
return { minimumAge: 18, ofac: false };
default:
return { minimumAge: 18, ofac: true };
}
}
async getActionId(userIdentifier: string, userDefinedData?: string) {
// Select config based on user data
return userDefinedData === 'premium' ? 'strict' : 'relaxed';
}
}
Best Practices
1. Configuration Management
- Store configurations in a database for easy updates
- Version your configurations for rollback capability
- Use meaningful config IDs (not just hashes)
- Document configuration requirements
2. Error Handling
try {
const result = await verifier.verify(/* params */);
} catch (error) {
if (error.name === 'ConfigMismatchError') {
// Log detailed issues for debugging
console.error('Config issues:', error.issues);
// Return user-friendly message
return { error: 'Verification configuration error' };
}
// Handle other errors
}
3. Security Considerations
- Always validate attestation IDs
- Store and check nullifiers to prevent replay
- Use appropriate scopes for different use cases
- Never expose configuration details to frontend
4. Performance Optimization
- Cache configuration objects
- Reuse verifier instances
- Batch verification requests when possible
- Use connection pooling for RPC calls
Resources
- Quickstart Guide - Basic V2 setup
- Basic Integration - Contract examples
- Workshop Example - Simple implementation
Need Help?
If you encounter issues during migration:
- Review example implementations
- Report issues at GitHub Issues