Plugin
Wallet Integration
Connect your Web3 wallet to receive PRIV token earnings.
Wallet Integration
The PRIV extension connects to your Web3 wallet to receive earnings. This guide covers the connection flow, account management, and best practices.
Supported Wallets
| Wallet | Status | Notes |
|---|---|---|
| MetaMask | Supported | Recommended |
| Coinbase Wallet | Supported | Via browser extension |
| Brave Wallet | Supported | Built into Brave browser |
| Rainbow | Supported | Via WalletConnect |
| Trust Wallet | Supported | Via WalletConnect |
Any wallet that injects the standard window.ethereum provider will work with the extension.
Connection Flow
Overview
sequenceDiagram
participant User
participant Popup as Extension Popup
participant Wallet as Web3 Wallet
participant BG as Background Script
participant API as PRIV API
User->>Popup: Click "Connect Wallet"
Popup->>Wallet: eth_requestAccounts
Wallet-->>User: Approve Connection
User->>Wallet: Approve
Wallet-->>Popup: Return Address
Popup->>BG: WALLET_CONNECTED
BG->>API: Register/Verify Wallet
API-->>BG: Success
BG-->>Popup: Connection CompleteStep-by-Step
- User Initiates: Click "Connect Wallet" in the extension
- Wallet Prompt: Your wallet opens a connection request
- User Approves: Approve the connection in your wallet
- Address Retrieved: Extension receives your wallet address
- Backend Sync: Address is registered with PRIV servers
- Ready: You can now receive earnings
Connecting Your Wallet
Prerequisites
- A Web3 wallet extension installed (MetaMask recommended)
- At least one account in your wallet
- The wallet should be on a supported network (Base recommended)
Connection Steps
- Open the PRIV extension by clicking its icon
- Click the Connect Wallet button in the header
- Your wallet extension will open with a connection request
- Select the account you want to use
- Click Connect in your wallet
- Your address will appear in the extension header
[Screenshot: Extension showing Connect Wallet button and MetaMask approval popup]
Code Implementation
Checking Wallet Availability
/**
* Check if wallet provider is available
*/
function isWalletAvailable(): boolean {
return typeof window !== 'undefined' && !!window.ethereum;
}Requesting Connection
/**
* Request wallet connection
*/
async function connectWallet(): Promise<WalletState> {
const provider = window.ethereum;
if (!provider) {
return {
isConnected: false,
address: null,
error: 'No wallet found. Please install MetaMask.',
};
}
try {
// Request account access
const accounts = await provider.request({
method: 'eth_requestAccounts',
}) as string[];
// Get current chain
const chainIdHex = await provider.request({
method: 'eth_chainId',
}) as string;
const chainId = parseInt(chainIdHex, 16);
const address = accounts[0] ?? null;
return {
isConnected: true,
address,
chainId,
error: null,
};
} catch (error) {
return {
isConnected: false,
address: null,
error: error.message,
};
}
}Wallet State Interface
interface WalletState {
isConnected: boolean;
address: string | null;
chainId: number | null;
isConnecting: boolean;
error: string | null;
}Account Change Handling
The extension automatically handles account changes in your wallet.
Listening for Changes
/**
* Listen for account changes
*/
function onAccountsChanged(
callback: (accounts: string[]) => void
): () => void {
const provider = window.ethereum;
if (!provider) {
return () => {};
}
const handler = (accounts: unknown) => {
callback(accounts as string[]);
};
provider.on('accountsChanged', handler);
// Return unsubscribe function
return () => provider.removeListener('accountsChanged', handler);
}Handling Account Changes
When you switch accounts in your wallet:
- Extension detects the change via
accountsChangedevent - New address is validated and sanitized
- Settings are updated with new address
- Background script is notified
- Cache is invalidated to refresh data
- UI updates to show new account
// In the popup App component
useEffect(() => {
const unsubscribe = onAccountsChanged(async (accounts) => {
if (accounts.length === 0) {
// Wallet disconnected
await handleDisconnect();
} else if (accounts[0] !== currentAddress) {
// Account changed
await handleAccountChange(accounts[0]);
}
});
return () => unsubscribe();
}, []);Address Validation
All wallet addresses are validated before use.
Validation Function
/**
* Validate and sanitize wallet address
*/
function sanitizeWalletAddress(
address: string | null | undefined
): string | null {
if (!address) return null;
const trimmed = address.trim();
// Validate Ethereum address format
if (!/^0x[a-fA-F0-9]{40}$/.test(trimmed)) {
return null;
}
// Normalize to lowercase
return trimmed.toLowerCase();
}Validation Rules
| Rule | Description |
|---|---|
| Prefix | Must start with 0x |
| Length | Exactly 42 characters |
| Characters | Only hexadecimal (0-9, a-f) |
| Case | Normalized to lowercase |
Disconnect Process
User-Initiated Disconnect
- Open extension settings or header menu
- Click Disconnect Wallet
- Confirm disconnection
- Wallet address is cleared from storage
- Data sharing is automatically disabled
Code Flow
/**
* Disconnect wallet
*/
async function handleDisconnectWallet() {
// Clear wallet from local storage
await clearWalletFromStorage();
// Update settings
await updateSettings({
walletAddress: null,
dataSharingEnabled: false
});
// Notify background script
await chrome.runtime.sendMessage({ type: 'WALLET_DISCONNECTED' });
// Reset earnings display
setEarnings(EMPTY_EARNINGS);
setBalance(EMPTY_BALANCE);
}What Gets Cleared
- Wallet address from settings
- Cached earnings data
- Sync state
- Data sharing preference
What Remains
- Collected data (local, unsubmitted)
- Other settings (notifications, etc.)
- Server-side earnings (accessible with same wallet)
Network Handling
Supported Networks
| Network | Chain ID | Status |
|---|---|---|
| Base | 8453 | Production |
| Base Sepolia | 84532 | Testnet |
Chain Detection
/**
* Get current chain ID
*/
async function getChainId(): Promise<number | null> {
const provider = window.ethereum;
if (!provider) return null;
try {
const chainIdHex = await provider.request({
method: 'eth_chainId',
}) as string;
return parseInt(chainIdHex, 16);
} catch {
return null;
}
}Switching Networks
/**
* Switch to Base Sepolia network
*/
async function switchToBaseSepolia(): Promise<boolean> {
const provider = window.ethereum;
const BASE_SEPOLIA_CHAIN_ID = '0x14a34'; // 84532
try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: BASE_SEPOLIA_CHAIN_ID }],
});
return true;
} catch (error) {
// Chain not added - add it
if (error.code === 4902) {
await provider.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: BASE_SEPOLIA_CHAIN_ID,
chainName: 'Base Sepolia',
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: ['https://sepolia.base.org'],
blockExplorerUrls: ['https://sepolia.basescan.org'],
}],
});
return true;
}
return false;
}
}Wallet Verification
For enhanced security, users can verify wallet ownership.
Verification Flow
sequenceDiagram
participant User
participant Extension
participant Wallet
participant API
Extension->>Extension: Generate Message
Extension->>Wallet: Request Signature
Wallet-->>User: Sign Request
User->>Wallet: Approve
Wallet-->>Extension: Signature
Extension->>API: Verify Signature
API-->>Extension: Auth TokenVerification Message
function generateVerificationMessage(address: string): string {
const timestamp = Date.now();
const nonce = Math.random().toString(36).substring(2, 15);
return `PRIV Protocol Wallet Verification
Address: ${address}
Timestamp: ${timestamp}
Nonce: ${nonce}
By signing this message, you verify ownership of this wallet address for use with PRIV Protocol.
This signature does not authorize any blockchain transactions.`;
}Signing Process
async function signMessage(
message: string,
address: string
): Promise<{ signature: string | null; error: string | null }> {
const provider = window.ethereum;
try {
const signature = await provider.request({
method: 'personal_sign',
params: [message, address],
}) as string;
return { signature, error: null };
} catch (error) {
return { signature: null, error: error.message };
}
}Display Formatting
Address Formatting
/**
* Format wallet address for display
*/
function formatAddress(address: string): string {
if (address.length < 10) return address;
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
// Example: 0x1234...5678Balance Formatting
/**
* Format token amount with decimals
*/
function formatTokenAmount(amount: bigint, decimals = 18): string {
const divisor = BigInt(10 ** decimals);
const wholePart = amount / divisor;
const fractionalPart = amount % divisor;
if (fractionalPart === BigInt(0)) {
return wholePart.toString();
}
const fractionalStr = fractionalPart
.toString()
.padStart(decimals, '0')
.slice(0, 4)
.replace(/0+$/, '');
return fractionalStr
? `${wholePart}.${fractionalStr}`
: wholePart.toString();
}
// Example: 1234.5678 PRIVError Handling
Common Errors
| Error | Cause | Solution |
|---|---|---|
| No wallet found | No provider injected | Install MetaMask |
| User rejected | User denied connection | Try again |
| Already pending | Connection request open | Check wallet |
| Unauthorized | Account not connected | Reconnect |
Error Handling Code
try {
const walletState = await connectWallet();
if (walletState.error) {
if (walletState.error.includes('User rejected')) {
showMessage('Connection cancelled');
} else if (walletState.error.includes('No wallet')) {
showMessage('Please install MetaMask');
} else {
showMessage(`Error: ${walletState.error}`);
}
}
} catch (error) {
console.error('Wallet connection failed:', error);
}Security Best Practices
Do
- Always validate addresses before use
- Show abbreviated addresses to users
- Handle account changes promptly
- Clear sensitive data on disconnect
Don't
- Store private keys
- Request unnecessary permissions
- Sign transactions without user consent
- Trust unvalidated addresses
Next Steps
- Earnings - Track and claim your earnings
- Privacy & Security - Security measures
- Troubleshooting - Common wallet issues