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
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ DISCORD_CALLBACK_URL=http://localhost:3000/v1/campaigns/first-squeezer/discord/c
# IMPORTANT: This address must match CAMPAIGN_SIGNER_ADDRESS in smart-contracts/.env
CAMPAIGN_SIGNER_PRIVATE_KEY=your_campaign_signer_private_key_here

# Bridge Ponder APIs (for protocol stats)
JUICEDOLLAR_PONDER_URL=https://ponder.juicedollar.com
# Bridge Ponder API (for protocol stats)
LDS_PONDER_URL=https://lightning.space/v1/claim

# Pinata IPFS Configuration (Launchpad Token Metadata)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ Returns TVL and 24h volume for the Explore page. All values are in USD.

Sum of V2 + V3 + Bridge 24h volume.

| Component | Source | Method |
| ---------- | --------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **V3** | `ExploreStatsService` (pre-computed `volume1Day` per pool) | Sum of per-pool 24h USD volumes |
| **V2** | `ExploreStatsService` (pre-computed `volume1Day` per pool) | Sum of per-pool 24h USD volumes |
| **Bridge** | JuiceDollar Ponder (`bridgeVolumeStats`) + LDS Ponder (`volumeStats`) | Stablecoin bridges: JUSD at $1. LDS: cBTC × BTC price + JUSD at $1 |
| Component | Source | Method |
| ---------- | ---------------------------------------------------------- | ---------------------------------- |
| **V3** | `ExploreStatsService` (pre-computed `volume1Day` per pool) | Sum of per-pool 24h USD volumes |
| **V2** | `ExploreStatsService` (pre-computed `volume1Day` per pool) | Sum of per-pool 24h USD volumes |
| **Bridge** | LDS Ponder (`volumeStats`) | LDS: cBTC × BTC price + JUSD at $1 |

### TVL

Expand Down
57 changes: 3 additions & 54 deletions src/services/ProtocolStatsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ interface StatsCache {
* ProtocolStatsService - Aggregates TVL and volume across V2, V3, and bridge
*
* V2/V3 stats: Derived by summing per-pool data from ExploreStatsService
* Bridge stats: Fetched directly via RPC multicall and external Ponder instances
* Bridge stats: Fetched directly via RPC multicall and the LDS Ponder instance
*/
export class ProtocolStatsService {
private logger: Logger;
Expand Down Expand Up @@ -162,13 +162,13 @@ export class ProtocolStatsService {

/**
* Bridge stats: TVL from StablecoinBridge minted() totals,
* volume from JuiceDollar Ponder + LDS Ponder GraphQL queries
* volume from the LDS Ponder GraphQL query
*/
private async getBridgeStats(chainId: number): Promise<ProtocolStats> {
try {
const [tvlUsd, volume24hUsd] = await Promise.all([
this.getBridgeTvl(chainId),
this.getBridgeVolume(chainId),
this.getLdsBridgeVolume(chainId),
]);

this.logger.info(
Expand Down Expand Up @@ -231,57 +231,6 @@ export class ProtocolStatsService {
}
}

/**
* Bridge volume = stablecoin bridge volume + LDS bridge volume (24h).
*/
private async getBridgeVolume(chainId: number): Promise<number> {
const [stablecoinVolume, ldsVolume] = await Promise.all([
this.getStablecoinBridgeVolume(),
this.getLdsBridgeVolume(chainId),
]);
return stablecoinVolume + ldsVolume;
}

/**
* Query JuiceDollar Ponder for rolling 24h stablecoin bridge volume.
* Uses hourly buckets with a 24h-ago cutoff. All values are JUSD (18 decimals, $1).
*/
private async getStablecoinBridgeVolume(): Promise<number> {
try {
const baseUrl =
process.env.JUICEDOLLAR_PONDER_URL || "https://ponder.juicedollar.com";
const cutoff = this.get24hAgoCutoff();
const query = `
query {
bridgeVolumeStats(where: { type: "1h", timestamp_gte: "${cutoff}" }, orderBy: "timestamp", orderDirection: "desc", limit: 200) {
items { stablecoinAddress, timestamp, volume, type }
}
}
`;

const response = await axios.post(
`${baseUrl}/graphql`,
{ query },
{ timeout: 10000 },
);

const items = response.data?.data?.bridgeVolumeStats?.items || [];
let totalVolume = 0;
for (const item of items) {
totalVolume += parseFloat(
ethers.utils.formatUnits(item.volume || "0", 18),
);
}
return totalVolume;
} catch (error) {
this.logger.warn(
{ error },
"Failed to fetch stablecoin bridge volume from JuiceDollar Ponder",
);
return 0;
}
}

/**
* Query LDS Ponder for rolling 24h bridge volume (BTC/Lightning/ERC20 atomic swaps).
* Uses hourly buckets with a 24h-ago cutoff.
Expand Down
133 changes: 133 additions & 0 deletions src/services/__tests__/ProtocolStatsService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ChainId } from "@juiceswapxyz/sdk-core";
import Logger from "bunyan";
import axios from "axios";
import { ethers } from "ethers";
import { ProtocolStatsService } from "../ProtocolStatsService";
import type { ExploreStatsService } from "../ExploreStatsService";

jest.mock("axios");
jest.mock("../PriceService", () => ({
PriceService: jest.fn().mockImplementation(() => ({
getBtcPriceUsd: jest.fn().mockResolvedValue(100000),
})),
}));

const mockedAxios = axios as jest.Mocked<typeof axios>;

function createMockLogger(): Logger {
const logger = {
child: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
logger.child.mockReturnValue(logger);
return logger as unknown as Logger;
}

function createService() {
const exploreStatsService = {
getExploreStats: jest
.fn()
.mockResolvedValue({ stats: { poolStatsV2: [], poolStatsV3: [] } }),
} as unknown as ExploreStatsService;

// Empty provider map → getBridgeTvl short-circuits to 0 without a multicall,
// so every axios.post seen here belongs to a bridge volume leg.
return new ProtocolStatsService(
new Map<ChainId, ethers.providers.StaticJsonRpcProvider>(),
createMockLogger(),
exploreStatsService,
);
}

const LDS_PONDER_URL = "https://lds-ponder.test/v1/claim";

describe("ProtocolStatsService bridge volume", () => {
const originalLdsPonderUrl = process.env.LDS_PONDER_URL;

afterAll(() => {
if (originalLdsPonderUrl === undefined) {
delete process.env.LDS_PONDER_URL;
} else {
process.env.LDS_PONDER_URL = originalLdsPonderUrl;
}
});

beforeEach(() => {
jest.clearAllMocks();
process.env.LDS_PONDER_URL = LDS_PONDER_URL;
// Answers a JuiceDollar `bridgeVolumeStats` query with a non-zero volume, so
// a resurrected JuiceDollar leg would show up as 5 + 7 instead of 7.
mockedAxios.post.mockImplementation(
async (_url: string, body?: unknown) => {
const query = (body as { query?: string })?.query ?? "";
if (query.includes("bridgeVolumeStats")) {
return {
data: {
data: {
bridgeVolumeStats: {
items: [
{
stablecoinAddress:
"0x0000000000000000000000000000000000000002",
timestamp: "1700000000",
volume: ethers.utils.parseUnits("5", 18).toString(),
type: "1h",
},
],
},
},
},
};
}
if (!query.includes("volumeStats")) {
throw new Error(`Unexpected Ponder query: ${query}`);
}
return {
data: {
data: {
volumeStats: {
items: [
{
tokenAddress: "0x0000000000000000000000000000000000000001",
timestamp: "1700000000",
volume: ethers.utils.parseUnits("7", 18).toString(),
type: "1h",
},
],
},
},
},
};
},
);
});

it("queries only the LDS Ponder, never the JuiceDollar Ponder", async () => {
const service = createService();

await service.getProtocolStats(ChainId.CITREA_MAINNET);

expect(mockedAxios.post).toHaveBeenCalledTimes(1);
const [url, body] = mockedAxios.post.mock.calls[0];
expect(url).toBe(`${LDS_PONDER_URL}/graphql`);
const { query } = body as { query: string };
expect(query).toContain("volumeStats");
expect(query).toMatch(
new RegExp(`\\bchainId\\s*:\\s*${ChainId.CITREA_MAINNET}\\b`),
);
expect(JSON.stringify({ url, body })).not.toMatch(
/juicedollar|bridgeVolumeStats/i,
);
});

it("reports bridge volume as the LDS leg alone", async () => {
const service = createService();

const stats = await service.getProtocolStats(ChainId.CITREA_MAINNET);

expect(stats.historicalProtocolVolume.Month.bridge[0].value).toBe(7);
});
});
Loading