The DeriverseTradeService fetches and parses on-chain trade history from Solana transactions, converting blockchain data into structured Trade objects with PnL, fees, and prices.
Overview
This service uses the @deriverse/kit Engine to decode transaction logs from the Deriverse program, extracting spot and perpetual trade fills with all relevant details.
Architecture
DeriverseTradeService
fetchTradesForWallet
Fetch and parse all Deriverse trades for a wallet address.
async fetchTradesForWallet(
connection: Connection,
walletAddress: string
): Promise<Trade[]>
Solana web3.js Connection instance
Solana wallet address to fetch trades for
Array of parsed trades sorted by closedAt descending (most recent first)
Configuration Parameters
The method uses several configuration constants:
| Parameter | Value | Description |
|---|
limit | 1000 | Maximum transaction signatures to fetch |
batchSize | 5 | Number of transactions per parallel batch |
maxTxs | 100 | Maximum transactions to process |
delayMs | 600 | Delay between batches (rate limiting) |
Trade Parsing Logic
Spot Trades
Detected when log message contains: orderId, qty, price, crncy, rebates, side
{
id: `${txSignature}-${orderId}-spot`,
symbol: INSTRUMENT_ID_TO_SYMBOL[instrId] ?? `Instr-${instrId}`,
quoteCurrency: 'USDC',
side: side === 0 ? 'buy' : 'sell',
orderType: 'limit',
quantity: qty / ASSET_DECIMALS,
price: price / PRICE_DECIMALS,
notional: (qty / ASSET_DECIMALS) * (price / PRICE_DECIMALS),
pnl: 0,
fee: -rebates / QUOTE_DECIMALS,
feeCurrency: 'quote',
openedAt: closedAt,
closedAt: closedAt,
durationSeconds: 0,
isWin: false,
txSignature
}
Perpetual Trades
Detected when log message contains: orderId, perps, price, crncy, rebates, side
{
id: `${txSignature}-${orderId}-perp`,
symbol: INSTRUMENT_ID_TO_SYMBOL[instrId] ?? `Instr-${instrId}`,
quoteCurrency: 'USDC',
side: side === 0 ? 'long' : 'short',
orderType: 'limit',
quantity: Math.abs(perps) / ASSET_DECIMALS,
price: price / PRICE_DECIMALS,
notional: (Math.abs(perps) / ASSET_DECIMALS) * (price / PRICE_DECIMALS),
pnl: crncy / QUOTE_DECIMALS,
fee: -rebates / QUOTE_DECIMALS,
feeCurrency: 'quote',
openedAt: closedAt,
closedAt: closedAt,
durationSeconds: 0,
isWin: (crncy / QUOTE_DECIMALS) > 0,
txSignature
}
Trade Object Structure
Parsed trade objectUnique identifier: {txSignature}-{orderId}-{spot|perp}
Trading pair symbol (e.g., ‘BTC-PERP’, ‘SOL-PERP’)
Quote currency (always ‘USDC’ for Deriverse)
side
'buy' | 'sell' | 'long' | 'short'
Trade side (buy/sell for spot, long/short for perps)
Order type (always ‘limit’ currently)
Trade quantity in base asset units
Trade value (quantity * price)
Profit and loss (0 for spot, calculated for perps)
Trading fee paid (negative rebates)
Fee currency (always ‘quote’)
Position opened timestamp (same as closedAt for now)
Position closed timestamp (transaction block time)
Position duration (0 for immediate fills)
Whether trade was profitable (false for spot, pnl > 0 for perps)
Solana transaction signature
Example Usage
Basic Trade Fetching
import { Connection } from '@solana/web3.js';
import { DeriverseTradeService } from '@/services/DeriverseTradeService';
const connection = new Connection(
process.env.NEXT_PUBLIC_RPC_HTTP!,
'confirmed'
);
const tradeService = new DeriverseTradeService();
const walletAddress = '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU';
const trades = await tradeService.fetchTradesForWallet(
connection,
walletAddress
);
console.log(`Found ${trades.length} trades`);
// Calculate statistics
const totalPnl = trades.reduce((sum, t) => sum + t.pnl, 0);
const totalFees = trades.reduce((sum, t) => sum + t.fee, 0);
const winningTrades = trades.filter(t => t.isWin).length;
const winRate = (winningTrades / trades.length) * 100;
console.log(`Total PnL: ${totalPnl.toFixed(2)} USDC`);
console.log(`Total Fees: ${totalFees.toFixed(2)} USDC`);
console.log(`Win Rate: ${winRate.toFixed(1)}%`);
Filtering by Symbol
const trades = await tradeService.fetchTradesForWallet(connection, walletAddress);
// Get only BTC perpetual trades
const btcTrades = trades.filter(t => t.symbol === 'BTC-PERP');
// Get only spot trades
const spotTrades = trades.filter(t => t.side === 'buy' || t.side === 'sell');
// Get only perpetual trades
const perpTrades = trades.filter(t => t.side === 'long' || t.side === 'short');
Persisting to Database
import { DeriverseTradeService } from '@/services/DeriverseTradeService';
import { SupabaseTradeService } from '@/services/SupabaseTradeService';
const tradeService = new DeriverseTradeService();
const supabaseService = new SupabaseTradeService();
async function syncTrades(walletAddress: string) {
// Fetch trades from blockchain
const trades = await tradeService.fetchTradesForWallet(
connection,
walletAddress
);
// Save to database
const result = await supabaseService.saveTrades(walletAddress, trades);
console.log(`Saved ${result.saved} trades to database`);
return result;
}
Real-time Dashboard Integration
import { useEffect, useState } from 'react';
import { DeriverseTradeService } from '@/services/DeriverseTradeService';
import { getRpcConnection } from '@/lib/utils';
function TradesPage() {
const [trades, setTrades] = useState<Trade[]>([]);
const [loading, setLoading] = useState(true);
const walletAddress = 'your-wallet-address';
useEffect(() => {
const fetchTrades = async () => {
try {
const connection = getRpcConnection();
const service = new DeriverseTradeService();
const result = await service.fetchTradesForWallet(
connection,
walletAddress
);
setTrades(result);
} catch (error) {
console.error('Error fetching trades:', error);
} finally {
setLoading(false);
}
};
fetchTrades();
}, [walletAddress]);
if (loading) return <div>Loading trades...</div>;
return (
<div>
<h1>Trade History</h1>
<table>
<thead>
<tr>
<th>Time</th>
<th>Symbol</th>
<th>Side</th>
<th>Quantity</th>
<th>Price</th>
<th>PnL</th>
<th>Fee</th>
</tr>
</thead>
<tbody>
{trades.map(trade => (
<tr key={trade.id}>
<td>{trade.closedAt.toLocaleString()}</td>
<td>{trade.symbol}</td>
<td>{trade.side}</td>
<td>{trade.quantity.toFixed(4)}</td>
<td>${trade.price.toFixed(2)}</td>
<td className={trade.pnl >= 0 ? 'positive' : 'negative'}>
${trade.pnl.toFixed(2)}
</td>
<td>${trade.fee.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Constants and Configuration
The service uses these constants from ../lib/constants:
export const PROGRAM_ID = 'deriverse_program_id';
export const DERIVERSE_VERSION = 1;
export const RPC_HTTP = process.env.NEXT_PUBLIC_RPC_HTTP!;
// Decimal scaling factors
export const PRICE_DECIMALS = 1e6; // 6 decimals
export const ASSET_DECIMALS = 1e9; // 9 decimals
export const QUOTE_DECIMALS = 1e6; // 6 decimals (USDC)
// Instrument ID to symbol mapping
export const INSTRUMENT_ID_TO_SYMBOL: Record<number, string> = {
0: 'BTC-PERP',
1: 'ETH-PERP',
2: 'SOL-PERP',
// ... more mappings
};
Batch Processing
Transactions are fetched in batches of 5 with 600ms delays to avoid rate limits:
for (let i = 0; i < Math.min(signatures.length, maxTxs); i += batchSize) {
if (i > 0) await new Promise(r => setTimeout(r, delayMs));
const batch = signatures.slice(i, i + batchSize);
// Process batch in parallel
}
Efficient Filtering
Only processes transactions with program data logs:
if (!tx?.meta?.logMessages?.some(isProgramLog)) continue;
Engine Singleton
Engine is initialized once and reused:
let enginePromise: Promise<Engine> | null = null;
async function getEngine(): Promise<Engine> {
if (enginePromise) return enginePromise;
enginePromise = (async () => {
// Initialize engine
})();
return enginePromise;
}
Error Handling
The service handles errors gracefully:
- Invalid wallet addresses throw errors early
- Failed transaction fetches are logged and skipped
- Log decode failures are caught per-transaction
- Successful trades are returned even if some transactions fail