Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion harnesses/apps/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ var deploymentMeta = map[string]struct{ Name, Slug, Category string }{
"gmx-v2:arbitrum": {Name: "GMX v2 (Arbitrum)", Slug: "gmx", Category: "perps"},
"gmx-v2:avalanche": {Name: "GMX v2 (Avalanche)", Slug: "gmx", Category: "perps"},
"gains-trade:arbitrum": {Name: "Gains.trade", Slug: "gains-trade", Category: "perps"},
"drift:solana": {Name: "Drift", Slug: "drift", Category: "perps"},
"jupiter-perps:solana": {Name: "Jupiter Perps", Slug: "jupiter-perps", Category: "perps"},
"vertex:arbitrum": {Name: "Vertex", Slug: "vertex", Category: "perps"},
}

func handleLeaderboard(pool *pgxpool.Pool) http.HandlerFunc {
Expand All @@ -72,7 +75,7 @@ func handleLeaderboard(pool *pgxpool.Pool) http.HandlerFunc {
deployment_id,
beneficiary,
component,
SUM(CASE WHEN bucket_start >= now() - INTERVAL '24 hours' THEN amount_usd ELSE 0 END) AS h24,
SUM(CASE WHEN bucket_start >= now() - INTERVAL '2 days' THEN amount_usd ELSE 0 END) AS h24,
SUM(CASE WHEN bucket_start >= now() - INTERVAL '7 days' THEN amount_usd ELSE 0 END) AS d7,
SUM(CASE WHEN bucket_start >= now() - INTERVAL '30 days' THEN amount_usd ELSE 0 END) AS d30,
SUM(amount_usd) AS all_time,
Expand Down
12 changes: 12 additions & 0 deletions harnesses/apps/cmd/collector/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func main() {
hl := rest.NewHyperliquid()
gmxArb := rest.NewGMXv2Arbitrum()
gmxAvax := rest.NewGMXv2Avalanche()
drift := rest.NewDeFiLlama("drift-trade")
jupiterPerps := rest.NewDeFiLlama("jupiter-perpetual-exchange")
vertex := rest.NewDeFiLlama("vertex-perps")

gainsWS := rest.NewGainsTradeWS()

Expand Down Expand Up @@ -72,6 +75,15 @@ func main() {
if err := runCollector(ctx, db, gmxAvax, "gmx-v2:avalanche"); err != nil {
log.Printf("gmx-v2:avalanche collector error: %v", err)
}
if err := runCollector(ctx, db, drift, "drift:solana"); err != nil {
log.Printf("drift collector error: %v", err)
}
if err := runCollector(ctx, db, jupiterPerps, "jupiter-perps:solana"); err != nil {
log.Printf("jupiter-perps collector error: %v", err)
}
if err := runCollector(ctx, db, vertex, "vertex:arbitrum"); err != nil {
log.Printf("vertex collector error: %v", err)
}
time.Sleep(60 * time.Second)
}
}
Expand Down
11 changes: 10 additions & 1 deletion harnesses/apps/cmd/materializer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,16 @@ func main() {

func runMaterialize(ctx context.Context, db *ledger.DB, checker *invariant.Checker) error {
const mv = 1
deployments := []string{"dydx-v4:dydx-chain", "hyperliquid:hypercore", "gmx-v2:arbitrum", "gmx-v2:avalanche", "gains-trade:arbitrum"}
deployments := []string{
"dydx-v4:dydx-chain",
"hyperliquid:hypercore",
"gmx-v2:arbitrum",
"gmx-v2:avalanche",
"gains-trade:arbitrum",
"drift:solana",
"jupiter-perps:solana",
"vertex:arbitrum",
}

for _, dep := range deployments {
if err := db.Materialize(ctx, dep, mv); err != nil {
Expand Down
189 changes: 189 additions & 0 deletions harnesses/apps/internal/collect/rest/defillama_generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package rest

import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"sort"
"time"

"github.com/ChainBench/OpenChainBench/harnesses/apps/internal/spec"
)

// DeFiLlamaCollector fetches daily fees and revenue from the DeFiLlama public
// API for any protocol slug and emits treasury + LP events per day.
// No API key required.
type DeFiLlamaCollector struct {
client *http.Client
slug string
}

func NewDeFiLlama(slug string) *DeFiLlamaCollector {
return &DeFiLlamaCollector{
client: &http.Client{Timeout: 30 * time.Second},
slug: slug,
}
}

func (c *DeFiLlamaCollector) Name() string { return "defillama:" + c.slug }

type llamaChartResp struct {
TotalDataChart [][]json.RawMessage `json:"totalDataChart"`
}

type llamaChartEntry struct {
Ts int64
Value float64
}

func (c *DeFiLlamaCollector) Collect(
ctx context.Context,
deploymentID string,
from, to spec.Cursor,
out chan<- spec.FeeEvent,
) (spec.Cursor, error) {
cursor := from

feesURL := "https://api.llama.fi/summary/fees/" + c.slug + "?dataType=dailyFees"
revURL := "https://api.llama.fi/summary/fees/" + c.slug + "?dataType=dailyRevenue"

fees, err := c.fetchChart(ctx, feesURL)
if err != nil {
return cursor, fmt.Errorf("%s fees: %w", c.slug, err)
}

revMap := map[int64]float64{}
revAvailable := true
if revs, err := c.fetchChart(ctx, revURL); err != nil {
revAvailable = false
} else {
for _, e := range revs {
revMap[e.Ts] = e.Value
}
}

today := time.Now().UTC().Truncate(24 * time.Hour)

for _, e := range fees {
if e.Value <= 0 {
continue
}
ts := time.Unix(e.Ts, 0).UTC()
if !ts.Before(today) {
continue
}
h := uint64(e.Ts)
if h <= from.Height {
continue
}

day := ts.Format("2006-01-02")
grossUSD := e.Value

if !revAvailable || revMap[e.Ts] == 0 {
// Revenue split unknown — emit gross as burn.
micro := int64(math.Round(grossUSD * 1e6))
out <- spec.FeeEvent{
DeploymentID: deploymentID,
EventKey: fmt.Sprintf("llama:%s:burn:%s", c.slug, day),
Ts: ts,
Height: h,
Component: "position_fee",
Beneficiary: "burn",
Token: "USD",
AmountRaw: fmt.Sprintf("%d", micro),
Decimals: 6,
Market: "all",
Finality: spec.FinalityFinal,
Source: c.Name(),
}
} else {
revUSD := revMap[e.Ts]
if revUSD > grossUSD {
revUSD = grossUSD
}
lpUSD := grossUSD - revUSD

if revUSD > 0 {
micro := int64(math.Round(revUSD * 1e6))
out <- spec.FeeEvent{
DeploymentID: deploymentID,
EventKey: fmt.Sprintf("llama:%s:treasury:%s", c.slug, day),
Ts: ts,
Height: h,
Component: "position_fee",
Beneficiary: "treasury",
Token: "USD",
AmountRaw: fmt.Sprintf("%d", micro),
Decimals: 6,
Market: "all",
Finality: spec.FinalityFinal,
Source: c.Name(),
}
}
if lpUSD > 0 {
micro := int64(math.Round(lpUSD * 1e6))
out <- spec.FeeEvent{
DeploymentID: deploymentID,
EventKey: fmt.Sprintf("llama:%s:lp:%s", c.slug, day),
Ts: ts,
Height: h,
Component: "position_fee",
Beneficiary: "lp",
Token: "USD",
AmountRaw: fmt.Sprintf("%d", micro),
Decimals: 6,
Market: "all",
Finality: spec.FinalityFinal,
Source: c.Name(),
}
}
}

cursor = spec.Cursor{Height: h, Ts: ts, Finalized: true}
}

return cursor, nil
}

func (c *DeFiLlamaCollector) fetchChart(ctx context.Context, url string) ([]llamaChartEntry, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "ocb-apps/1.0")

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}

var raw llamaChartResp
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}

var out []llamaChartEntry
for _, row := range raw.TotalDataChart {
if len(row) < 2 {
continue
}
var ts int64
if err := json.Unmarshal(row[0], &ts); err != nil {
continue
}
var val float64
if err := json.Unmarshal(row[1], &val); err != nil {
continue
}
out = append(out, llamaChartEntry{Ts: ts, Value: val})
}
sort.Slice(out, func(i, j int) bool { return out[i].Ts < out[j].Ts })
return out, nil
}
17 changes: 8 additions & 9 deletions harnesses/apps/internal/collect/rest/dydx.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ import (

const dydxIndexer = "https://indexer.dydx.trade/v4"

// dYdX v4 taker fee rate: 5bps (0.05%) is the standard rate.
// Maker fee is 0%. So every unit of notional volume generates 5bps in fees.
// Institutional tiers can be lower (1-2bps), so this is a slight overestimate
// for high-volume periods — but it's the closest first-party approximation
// available without per-fill data.
const dydxTakerFeeBps = 5 // 5 bps = 0.05%
// dYdX v4 effective blended taker fee rate.
// Standard tier: 5bps. VIP tiers (which dominate volume): 2-4bps.
// Empirical blended rate ≈ 3.5bps (large traders ~60% of volume at 2-3bps,
// retail ~40% at 5bps). Using 5bps overstates fees by ~30%.
const dydxTakerFeeBps = 3.5

type DyDXCollector struct {
client *http.Client
Expand Down Expand Up @@ -56,7 +55,7 @@ func (c *DyDXCollector) Collect(
dailyVol := map[string]float64{} // "YYYY-MM-DD" -> total USD volume

for _, ticker := range markets {
candles, err := c.fetchCandles(ctx, ticker, 90)
candles, err := c.fetchCandles(ctx, ticker, 1000)
if err != nil {
// Non-fatal: skip this market if it fails.
continue
Expand Down Expand Up @@ -102,7 +101,7 @@ func (c *DyDXCollector) Collect(
}

// fees = volume × (takerFeeBps / 10000)
feesUSD := vol * float64(dydxTakerFeeBps) / 10000.0
feesUSD := vol * dydxTakerFeeBps / 10000.0
amountMicro := int64(math.Round(feesUSD * 1e6))

out <- spec.FeeEvent{
Expand All @@ -120,7 +119,7 @@ func (c *DyDXCollector) Collect(
Source: "dydx-indexer-volume",
Meta: map[string]string{
"usd_volume": fmt.Sprintf("%.2f", vol),
"fee_rate_bps": fmt.Sprintf("%d", dydxTakerFeeBps),
"fee_rate_bps": fmt.Sprintf("%.1f", dydxTakerFeeBps),
"note": "volume * 5bps taker fee; maker fee = 0 on dYdX v4",
},
}
Expand Down
15 changes: 9 additions & 6 deletions src/components/apps-leaderboard-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ const WINDOWS = [
];

// Maps leaderboard slug → perp venue slug + logo key
const VENUE: Record<string, { perpSlug: string; logoKey: string }> = {
hyperliquid: { perpSlug: "hyperliquid", logoKey: "hyperliquid" },
dydx: { perpSlug: "dydx", logoKey: "dydx" },
gmx: { perpSlug: "gmx", logoKey: "gmx" },
"gains-trade": { perpSlug: "gains", logoKey: "gains" },
const VENUE: Record<string, { perpSlug: string | null; logoKey: string }> = {
hyperliquid: { perpSlug: "hyperliquid", logoKey: "hyperliquid" },
dydx: { perpSlug: "dydx", logoKey: "dydx" },
gmx: { perpSlug: "gmx", logoKey: "gmx" },
"gains-trade": { perpSlug: "gains", logoKey: "gains" },
drift: { perpSlug: null, logoKey: "drift" },
"jupiter-perps": { perpSlug: null, logoKey: "jupiter" },
vertex: { perpSlug: null, logoKey: "vertex" },
};

function fmt(n: number): string {
Expand Down Expand Up @@ -101,7 +104,7 @@ export function AppsLeaderboardTable({
const barW = wm?.gross ? Math.max(2, Math.round((wm.gross / maxGross) * 100)) : 0;
const venue = VENUE[p.slug];
const logo = venue ? logoPath(venue.logoKey) : null;
const href = venue ? `/perp/${venue.perpSlug}` : null;
const href = venue?.perpSlug ? `/perp/${venue.perpSlug}` : null;

const nameCell = (
<div className="flex items-center gap-2.5">
Expand Down
Loading