Wallet Integration
The wallet adapter is the bridge between the Privacy Boost SDK and your wallet integration. This guide covers implementing adapters for various wallet providers.Interface Definition
interface WalletAdapter {
/** Get the connected wallet address */
getAddress(): Promise<string>;
/** Get the current chain ID */
getChainId(): Promise<number>;
/** Sign a personal message (EIP-191) */
signMessage(message: string): Promise<string>;
/** Sign EIP-712 typed data */
signTypedData(typedDataJson: string): Promise<string>;
/** Send a transaction and return the tx hash */
sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string>;
/** Wait for a mined receipt and return the fields the SDK consumes */
waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}>;
}
Implementation Requirements
Return Values
- Addresses: Checksummed hex strings with
0xprefix (42 characters) - Signatures: Hex strings with
0xprefix (132 characters for 65 bytes: r + s + v) - Transaction hashes: Hex strings with
0xprefix (66 characters) - Receipts:
transactionHash, booleanstatus, and logs withaddress,topics, anddata - Chain ID: Number matching expected network
Error Handling
Throw standard JavaScript errors with descriptive messages:throw new Error('User rejected the signing request');
throw new Error('Transaction failed: insufficient funds');
MetaMask / Injected Provider
function createMetaMaskAdapter(): WalletAdapter {
if (typeof window.ethereum === 'undefined') {
throw new Error('MetaMask not installed');
}
const provider = window.ethereum;
return {
async getAddress(): Promise<string> {
const accounts = await provider.request({
method: 'eth_requestAccounts'
});
return accounts[0];
},
async getChainId(): Promise<number> {
const chainId = await provider.request({
method: 'eth_chainId'
});
return parseInt(chainId, 16);
},
async signMessage(message: string): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'personal_sign',
params: [message, address]
});
},
async signTypedData(typedDataJson: string): Promise<string> {
const address = await this.getAddress();
const typedData = JSON.parse(typedDataJson);
return provider.request({
method: 'eth_signTypedData_v4',
params: [address, JSON.stringify(typedData)]
});
},
async sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'eth_sendTransaction',
params: [{
from: address,
to: tx.to,
value: '0x' + BigInt(tx.value).toString(16),
data: tx.data
}]
});
},
async waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}> {
while (true) {
const receipt = await provider.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
});
if (receipt) {
return {
transactionHash: receipt.transactionHash ?? txHash,
status: receipt.status !== '0x0',
logs: (receipt.logs ?? []).map((log: any) => ({
address: log.address,
topics: log.topics ?? [],
data: log.data ?? '0x',
})),
};
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
};
}
WalletConnect
import { EthereumProvider } from '@walletconnect/ethereum-provider';
async function createWalletConnectAdapter(
projectId: string,
chainId: number
): Promise<WalletAdapter> {
const provider = await EthereumProvider.init({
projectId,
chains: [chainId],
showQrModal: true,
});
await provider.enable();
return {
async getAddress(): Promise<string> {
const accounts = provider.accounts;
if (accounts.length === 0) {
throw new Error('No accounts connected');
}
return accounts[0];
},
async getChainId(): Promise<number> {
return provider.chainId;
},
async signMessage(message: string): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'personal_sign',
params: [message, address]
});
},
async signTypedData(typedDataJson: string): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'eth_signTypedData_v4',
params: [address, typedDataJson]
});
},
async sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'eth_sendTransaction',
params: [{
from: address,
to: tx.to,
value: '0x' + BigInt(tx.value).toString(16),
data: tx.data
}]
});
},
async waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}> {
while (true) {
const receipt = await provider.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
});
if (receipt) {
return {
transactionHash: receipt.transactionHash ?? txHash,
status: receipt.status !== '0x0',
logs: (receipt.logs ?? []).map((log: any) => ({
address: log.address,
topics: log.topics ?? [],
data: log.data ?? '0x',
})),
};
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
};
}
Coinbase Wallet
import CoinbaseWalletSDK from '@coinbase/wallet-sdk';
function createCoinbaseAdapter(appName: string, rpcUrl: string): WalletAdapter {
const sdk = new CoinbaseWalletSDK({ appName });
const provider = sdk.makeWeb3Provider(rpcUrl);
return {
async getAddress(): Promise<string> {
const accounts = await provider.request({
method: 'eth_requestAccounts'
});
return accounts[0];
},
async getChainId(): Promise<number> {
const chainId = await provider.request({
method: 'eth_chainId'
});
return parseInt(chainId as string, 16);
},
async signMessage(message: string): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'personal_sign',
params: [message, address]
}) as Promise<string>;
},
async signTypedData(typedDataJson: string): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'eth_signTypedData_v4',
params: [address, typedDataJson]
}) as Promise<string>;
},
async sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string> {
const address = await this.getAddress();
return provider.request({
method: 'eth_sendTransaction',
params: [{
from: address,
to: tx.to,
value: '0x' + BigInt(tx.value).toString(16),
data: tx.data
}]
}) as Promise<string>;
},
async waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}> {
while (true) {
const receipt = await provider.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
});
if (receipt) {
return {
transactionHash: receipt.transactionHash ?? txHash,
status: receipt.status !== '0x0',
logs: (receipt.logs ?? []).map((log: any) => ({
address: log.address,
topics: log.topics ?? [],
data: log.data ?? '0x',
})),
};
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
};
}
Safe (Gnosis Safe)
import Safe from '@safe-global/protocol-kit';
import SafeApiKit from '@safe-global/api-kit';
async function createSafeAdapter(
safeAddress: string,
signerPrivateKey: string,
rpcUrl: string
): Promise<WalletAdapter> {
const safe = await Safe.create({
ethAdapter: /* your eth adapter */,
safeAddress
});
const apiKit = new SafeApiKit({
chainId: BigInt(await safe.getChainId())
});
return {
async getAddress(): Promise<string> {
return safeAddress;
},
async getChainId(): Promise<number> {
return Number(await safe.getChainId());
},
async signMessage(message: string): Promise<string> {
const messageHash = await safe.createMessage(message);
const signedMessage = await safe.signMessage(messageHash);
// For multi-sig, you may need to propose and collect signatures
return signedMessage.encodedSignatures();
},
async signTypedData(typedDataJson: string): Promise<string> {
const typedData = JSON.parse(typedDataJson);
// Safe SDK handles EIP-712 signing
const signature = await safe.signTypedData(typedData);
return signature.encodedSignatures();
},
async sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string> {
const safeTransaction = await safe.createTransaction({
transactions: [{
to: tx.to,
value: tx.value,
data: tx.data
}]
});
// Sign and execute (or propose for multi-sig)
const signedTx = await safe.signTransaction(safeTransaction);
const response = await safe.executeTransaction(signedTx);
return response.hash;
},
async waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}> {
const receipt = await safe.getEthAdapter().getProvider().waitForTransaction(txHash);
if (!receipt) {
throw new Error(`Transaction not found: ${txHash}`);
}
return {
transactionHash: receipt.transactionHash ?? receipt.hash ?? txHash,
status: receipt.status === 1 || receipt.status === 'success',
logs: (receipt.logs ?? []).map((log: any) => ({
address: log.address,
topics: log.topics ?? [],
data: log.data ?? '0x',
})),
};
}
};
}
Testing Adapter
For testing, create a mock adapter:import { Wallet } from 'ethers';
function createTestAdapter(privateKey: string): WalletAdapter {
const wallet = new Wallet(privateKey);
return {
async getAddress(): Promise<string> {
return wallet.address;
},
async getChainId(): Promise<number> {
return 1; // Mainnet
},
async signMessage(message: string): Promise<string> {
return wallet.signMessage(message);
},
async signTypedData(typedDataJson: string): Promise<string> {
const typedData = JSON.parse(typedDataJson);
const { domain, types, message: data } = typedData;
const { EIP712Domain, ...signingTypes } = types;
return wallet.signTypedData(domain, signingTypes, data);
},
async sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string> {
// Return mock transaction hash
return '0x' + '0'.repeat(64);
},
async waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}> {
return {
transactionHash: txHash,
status: true,
logs: [],
};
}
};
}
Adapter Factory
Create a unified factory for multiple wallet types:type WalletType = 'metamask' | 'walletconnect' | 'coinbase';
async function createAdapter(
type: WalletType,
options?: { projectId?: string; rpcUrl?: string }
): Promise<WalletAdapter> {
switch (type) {
case 'metamask':
return createMetaMaskAdapter();
case 'walletconnect':
if (!options?.projectId) throw new Error('projectId required');
return createWalletConnectAdapter(options.projectId, 1);
case 'coinbase':
if (!options?.rpcUrl) throw new Error('rpcUrl required');
return createCoinbaseAdapter('My App', options.rpcUrl);
default:
throw new Error(`Unknown wallet type: ${type}`);
}
}
Best Practices
- Validate chain ID - Check that the wallet is on the expected network
- Handle rejections - Users may reject signing requests
- Timeout long operations - Set reasonable timeouts for wallet interactions
- Cache address - Avoid repeated
getAddress()calls - Handle disconnections - Watch for wallet disconnect events
// Example with validation and caching
class ValidatingAdapter implements WalletAdapter {
private cachedAddress: string | null = null;
private expectedChainId: number;
constructor(
private inner: WalletAdapter,
expectedChainId: number
) {
this.expectedChainId = expectedChainId;
}
async getAddress(): Promise<string> {
if (!this.cachedAddress) {
this.cachedAddress = await this.inner.getAddress();
}
return this.cachedAddress;
}
async getChainId(): Promise<number> {
const chainId = await this.inner.getChainId();
if (chainId !== this.expectedChainId) {
throw new Error(
`Wrong network: expected ${this.expectedChainId}, got ${chainId}`
);
}
return chainId;
}
// ... delegate other methods with timeout wrapper
async signMessage(message: string): Promise<string> {
return this.inner.signMessage(message);
}
async signTypedData(typedDataJson: string): Promise<string> {
return this.inner.signTypedData(typedDataJson);
}
async sendTransaction(tx: {
to: string;
value: string;
data: string;
}): Promise<string> {
return this.inner.sendTransaction(tx);
}
async waitForTransactionReceipt(txHash: string): Promise<{
transactionHash: string;
status: boolean;
logs: Array<{ address: string; topics: string[]; data: string }>;
}> {
return this.inner.waitForTransactionReceipt(txHash);
}
}
Next Steps
- Deposits Guide - Deposit tokens into your private balance
- Withdrawals Guide - Withdraw tokens from your private balance
- Transfers Guide - Send private transfers
- API Reference - Complete API documentation