Bridge Wrapped aggregates bridging activity from three major bridge protocols to provide a complete view of your cross-chain transactions.
Supported bridges
Bridge Wrapped integrates with three leading bridge aggregators:
Across Protocol
Fast, secure bridging with optimistic verification
Relay
High-performance cross-chain transactions
LiFi
Multi-protocol aggregation for best rates
Architecture
The aggregation system uses a modular adapter pattern with a unified interface:
interface BridgeProviderAdapter {
readonly name: BridgeProvider;
fetchTransactions(
address: string,
startTimestamp: number,
endTimestamp: number
): Promise<NormalizedBridgeTransaction[]>;
}
Each bridge adapter implements this interface, ensuring consistent data normalization across all providers.
Bridge adapters
Across Protocol
The Across adapter fetches deposit data from the Across API:
src/services/bridges/across.ts
export class AcrossAdapter implements BridgeProviderAdapter {
readonly name = 'across' as const;
private baseUrl = API_URLS.ACROSS;
async fetchTransactions(
address: string,
startTimestamp: number,
endTimestamp: number
): Promise<NormalizedBridgeTransaction[]> {
const allTransactions: NormalizedBridgeTransaction[] = [];
let offset = 0;
let hasMore = true;
while (hasMore) {
const response = await retryWithBackoff(() =>
this.fetchPage(address, offset)
);
if (!response.deposits || response.deposits.length === 0) {
hasMore = false;
break;
}
for (const deposit of response.deposits) {
const normalized = await this.normalizeDeposit(deposit);
if (!normalized) continue;
if (isWithinYear(normalized.timestamp, startTimestamp, endTimestamp)) {
allTransactions.push(normalized);
}
if (normalized.timestamp < startTimestamp) {
hasMore = false;
break;
}
}
offset += PAGINATION.ACROSS_LIMIT;
}
return allTransactions;
}
}
Across deposits include price data directly from the API, reducing the need for external price lookups.
Relay
The Relay adapter uses cursor-based pagination:
src/services/bridges/relay.ts
export class RelayAdapter implements BridgeProviderAdapter {
readonly name = 'relay' as const;
private baseUrl = API_URLS.RELAY;
async fetchTransactions(
address: string,
startTimestamp: number,
endTimestamp: number
): Promise<NormalizedBridgeTransaction[]> {
const allTransactions: NormalizedBridgeTransaction[] = [];
let continuation: string | undefined;
let hasMore = true;
while (hasMore) {
const response = await retryWithBackoff(() =>
this.fetchPage(address, continuation)
);
if (!response.requests || response.requests.length === 0) {
hasMore = false;
break;
}
for (const request of response.requests) {
const normalized = await this.normalizeRequest(request);
if (!normalized) continue;
if (isWithinYear(normalized.timestamp, startTimestamp, endTimestamp)) {
allTransactions.push(normalized);
}
}
continuation = response.continuation;
if (!continuation) hasMore = false;
}
return allTransactions;
}
}
LiFi
The LiFi adapter leverages their analytics API:
src/services/bridges/lifi.ts
export class LiFiAdapter implements BridgeProviderAdapter {
readonly name = 'lifi' as const;
private baseUrl = API_URLS.LIFI;
async fetchTransactions(
address: string,
startTimestamp: number,
endTimestamp: number
): Promise<NormalizedBridgeTransaction[]> {
const allTransactions: NormalizedBridgeTransaction[] = [];
let nextCursor: string | undefined;
let hasMore = true;
while (hasMore) {
const response = await retryWithBackoff(() =>
this.fetchPage(address, startTimestamp, endTimestamp, nextCursor)
);
if (!response.transfers || response.transfers.length === 0) {
hasMore = false;
break;
}
for (const transfer of response.transfers) {
const normalized = await this.normalizeTransfer(transfer);
if (normalized) {
allTransactions.push(normalized);
}
}
nextCursor = response.hasNext ? response.next : undefined;
if (!nextCursor) hasMore = false;
}
return allTransactions;
}
}
LiFi’s API supports timestamp filtering directly, allowing for more efficient queries compared to client-side filtering.
Data normalization
All bridge transactions are normalized to a common format:
interface NormalizedBridgeTransaction {
id: string;
provider: 'across' | 'relay' | 'lifi';
txHash: string;
timestamp: number;
sourceChainId: number;
sourceChainName: string;
destinationChainId: number;
destinationChainName: string;
tokenSymbol: string;
tokenAddress: string;
amount: string;
amountFormatted: number;
amountUSD: number;
status: 'pending' | 'completed' | 'failed';
}
Token amount parsing
Token amounts are converted from raw values to human-readable decimals:
function parseTokenAmount(amount: string, decimals: number): number {
const value = BigInt(amount);
const divisor = BigInt(10 ** decimals);
return Number(value) / Number(divisor);
}
Price enrichment
Bridge Wrapped enhances transaction data with USD values using CoinMarketCap’s API:
const tokenInfo = await coinMarketCapService.getTokenInfo(tokenAddress);
const tokenSymbol = tokenInfo?.symbol || deposit.token?.symbol;
const tokenDecimals = tokenInfo?.decimals || deposit.token?.decimals || 18;
const amountFormatted = parseTokenAmount(deposit.inputAmount, tokenDecimals);
const amountUSD = amountFormatted * tokenPriceUSD;
Aggregation logic
The BridgeAggregator class orchestrates data fetching from all bridges:
src/services/bridges/aggregator.ts
export class BridgeAggregator {
async getWrappedStats(
address: string,
year: number = 2025
): Promise<BridgeWrappedStats> {
const startTimestamp = Math.floor(
new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000
);
const endTimestamp = Math.floor(
new Date(`${year}-12-31T23:59:59Z`).getTime() / 1000
);
// Fetch transactions from all providers in parallel
const [acrossTransactions, relayTransactions, lifiTransactions] =
await Promise.all([
acrossAdapter.fetchTransactions(address, startTimestamp, endTimestamp)
.catch((err) => {
console.error('Across fetch failed:', err);
return [];
}),
relayAdapter.fetchTransactions(address, startTimestamp, endTimestamp)
.catch((err) => {
console.error('Relay fetch failed:', err);
return [];
}),
lifiAdapter.fetchTransactions(address, startTimestamp, endTimestamp)
.catch((err) => {
console.error('LiFi fetch failed:', err);
return [];
}),
]);
// Combine all transactions
const allTransactions = [
...acrossTransactions,
...relayTransactions,
...lifiTransactions,
];
// Deduplicate transactions
const deduplicatedTransactions = this.deduplicateTransactions(allTransactions);
return this.calculateStats(address, year, deduplicatedTransactions);
}
}
Parallel fetching
All three bridge APIs are queried simultaneously using Promise.all() for optimal performance
Error handling
Individual bridge failures don’t crash the entire process - failed requests return empty arrays
Deduplication
Transactions are deduplicated based on transaction hash and chain pair
Statistics calculation
Normalized transactions are processed to generate comprehensive analytics
Deduplication
To prevent double-counting transactions that appear in multiple bridge APIs:
private deduplicateTransactions(
transactions: NormalizedBridgeTransaction[]
): NormalizedBridgeTransaction[] {
const seen = new Map<string, NormalizedBridgeTransaction>();
for (const tx of transactions) {
const key = `${tx.txHash.toLowerCase()}-${tx.sourceChainId}-${tx.destinationChainId}`;
if (!seen.has(key)) {
seen.set(key, tx);
} else {
// Prefer transaction with USD value
const existing = seen.get(key)!;
if (tx.amountUSD > 0 && existing.amountUSD === 0) {
seen.set(key, tx);
}
}
}
return Array.from(seen.values());
}
The deduplication key combines:
- Transaction hash (lowercase)
- Source chain ID
- Destination chain ID
When duplicates are found, Bridge Wrapped prefers the transaction with USD value data to ensure accurate volume calculations.
Statistics calculation
After aggregation and deduplication, the system calculates:
- Total bridging actions: Count of all transactions
- Total volume USD: Sum of all transaction values
- Chain statistics: Most used source/destination chains and highest volume chains
- Token statistics: Most bridged tokens and top tokens by count
- Temporal statistics: Busiest day and monthly activity patterns
- Provider breakdown: Transaction counts and volumes per bridge
return {
walletAddress: address,
year,
generatedAt: new Date().toISOString(),
totalBridgingActions,
totalVolumeUSD,
mostUsedSourceChain,
mostUsedDestinationChain,
highestVolumeDestination,
mostBridgedToken,
busiestDay,
providerBreakdown: providerStats,
monthlyActivity: formattedMonthlyActivity,
topSourceChains,
topDestinationChains,
topTokens,
transactions: sortedTransactions,
};
Error handling and retry logic
All API requests use exponential backoff for resilience:
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve =>
setTimeout(resolve, baseDelay * Math.pow(2, i))
);
}
}
throw new Error('Max retries exceeded');
}
The retry logic uses exponential backoff: 1s, 2s, 4s delays between attempts. This prevents overwhelming bridge APIs during temporary outages.
Fetching from all three bridges simultaneously reduces total loading time from ~15s to ~5s for typical wallets.
When transactions fall outside the requested time range, pagination stops early to avoid unnecessary API calls.
CoinMarketCap token lookups are batched using getMultipleTokenInfo() to reduce API calls.