Skip to content

Commit

Permalink
Merge branch 'main' into db-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
noahlitvin authored Feb 14, 2025
2 parents e5c4563 + 5d2a345 commit 5150051
Show file tree
Hide file tree
Showing 21 changed files with 352 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@apollo/server": "^4.11.3",
"@sentry/cli": "^2.39.0",
"@sentry/node": "^8.40.0",
"axios": "^1.7.7",
"class-validator": "^0.14.1",
"cors": "^2.8.5",
"dataloader": "^2.2.3",
Expand Down
1 change: 1 addition & 0 deletions packages/api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Query {
positions(chainId: Int, marketAddress: String, owner: String): [PositionType!]!
resource(slug: String!): ResourceType
resourceCandles(from: Int!, interval: Int!, slug: String!, to: Int!): [CandleType!]!
resourcePrices: [ResourcePriceType!]!
resourceTrailingAverageCandles(from: Int!, interval: Int!, slug: String!, to: Int!, trailingTime: Int!): [CandleType!]!
resources: [ResourceType!]!
transactions(positionId: Int): [TransactionType!]!
Expand Down
3 changes: 1 addition & 2 deletions packages/api/src/controllers/marketHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export const createOrUpdateEpochFromContract = async (
args,
});
const epochData: EpochData = (epochReadResult as EpochReadResult)[0];
console.log('epochReadResult', epochReadResult);

const _epochId = epochId || Number(epochData.epochId);

// check if epoch already exists in db
Expand Down Expand Up @@ -338,7 +338,6 @@ export const createOrUpdateEpochFromContract = async (
updatedEpoch.market = market;
updatedEpoch.marketParams = marketParams;
await epochRepository.save(updatedEpoch);
console.log('saved epoch:', updatedEpoch);
};

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mainnet, sepolia, base, cannon } from 'viem/chains';
import evmIndexer from './resourcePriceFunctions/evmIndexer';
import ethBlobsIndexer from './resourcePriceFunctions/ethBlobsIndexer';
import celestiaIndexer from './resourcePriceFunctions/celestiaIndexer';
import { Deployment, MarketInfo } from './interfaces';

Expand All @@ -18,6 +19,11 @@ export const RESOURCES = [
slug: 'ethereum-gas',
priceIndexer: new evmIndexer(mainnet.id),
},
{
name: 'Ethereum Blobspace',
slug: 'ethereum-blobspace',
priceIndexer: new ethBlobsIndexer(),
},
...(process.env.CELENIUM_API_KEY
? [
{
Expand Down
19 changes: 19 additions & 0 deletions packages/api/src/graphql/resolvers/ResourceResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import dataSource from '../../db';
import { Resource } from '../../models/Resource';
import { ResourceType } from '../types';
import { mapResourceToType } from './mappers';
import { ResourcePrice } from '../../models/ResourcePrice';
import { ResourcePriceType } from '../types';

@Resolver(() => ResourceType)
export class ResourceResolver {
Expand Down Expand Up @@ -35,4 +37,21 @@ export class ResourceResolver {
throw new Error('Failed to fetch resource');
}
}

@Query(() => [ResourcePriceType])
async resourcePrices(): Promise<ResourcePriceType[]> {
try {
const prices = await dataSource.getRepository(ResourcePrice).find({
relations: ['resource'],
});

return prices.map((price) => ({
...price,
resource: mapResourceToType(price.resource),
}));
} catch (error) {
console.error('Error fetching resource prices:', error);
throw new Error('Failed to fetch resource prices');
}
}
}
270 changes: 270 additions & 0 deletions packages/api/src/resourcePriceFunctions/ethBlobsIndexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { resourcePriceRepository } from '../db';
import Sentry from '../sentry';
import { IResourcePriceIndexer } from '../interfaces';
import { Resource } from 'src/models/Resource';
import axios from 'axios';

interface BlobData {
blobGasPrice: string;
blobGasUsed: string;
timestamp: number;
blockNumber: number;
}

class ethBlobsIndexer implements IResourcePriceIndexer {
public client = undefined; // Required by interface but not used for Blobscan
private isWatching: boolean = false;
private blobscanApiUrl: string = 'https://api.blobscan.com';
private retryDelay: number = 1000; // 1 second
private maxRetries: number = 3;

private async fetchBlobDataFromBlobscan(
blockNumber: number,
retryCount = 0
): Promise<BlobData | null> {
try {
const response = await axios.get(`${this.blobscanApiUrl}/blocks`, {
params: {
startBlock: blockNumber,
endBlock: blockNumber,
},
});

if (response.data?.blocks?.[0]) {
const block = response.data.blocks[0];
const timestamp = Math.floor(
new Date(block.timestamp).getTime() / 1000
);

return {
blobGasPrice: block.blobGasPrice || '0',
blobGasUsed: block.blobGasUsed || '0',
timestamp: timestamp,
blockNumber: blockNumber,
};
}
return null;
} catch (error) {
if (axios.isAxiosError(error)) {
// Handle rate limiting
if (error.response?.status === 429 && retryCount < this.maxRetries) {
console.warn(
`Rate limited when fetching block ${blockNumber}, retrying in ${this.retryDelay}ms...`
);
await new Promise((resolve) => setTimeout(resolve, this.retryDelay));
return this.fetchBlobDataFromBlobscan(blockNumber, retryCount + 1);
}

// Log specific error details
Sentry.withScope((scope) => {
scope.setExtra('blockNumber', blockNumber);
scope.setExtra('status', error.response?.status);
scope.setExtra('errorMessage', error.message);
Sentry.captureException(error);
});
}
console.warn(
`Failed to fetch blob data for block ${blockNumber}:`,
error
);
return null;
}
}

private formatGwei(wei: string): string {
try {
return (Number(wei) / 1e9).toFixed(9);
} catch {
return '0';
}
}

private async storeBlockPrice(blockNumber: number, resource: Resource) {
try {
const blobData = await this.fetchBlobDataFromBlobscan(blockNumber);
if (!blobData) {
console.warn(`No blob data for block ${blockNumber}. Skipping block.`);
return;
}

const used = blobData.blobGasUsed;
const feePaid =
BigInt(blobData.blobGasPrice) * BigInt(blobData.blobGasUsed);

const price = {
resource: { id: resource.id },
timestamp: blobData.timestamp,
value: blobData.blobGasPrice,
used: used,
feePaid: feePaid.toString(),
blockNumber: blockNumber,
};

await resourcePriceRepository.upsert(price, ['resource', 'timestamp']);
} catch (error) {
console.error('Error storing block price:', error);
Sentry.withScope((scope) => {
scope.setExtra('blockNumber', blockNumber);
scope.setExtra('resource', resource.slug);
Sentry.captureException(error);
});
}
}

async indexBlockPriceFromTimestamp(
resource: Resource,
timestamp: number
): Promise<boolean> {
try {
// Get recent blobs to find a reasonable block range
const response = await axios.get(`${this.blobscanApiUrl}/blobs`, {
params: {
ps: 1,
sort: 'desc',
},
});

if (!response.data?.blobs?.[0]?.blockNumber) {
console.error('Failed to get recent blob data');
return false;
}

const currentBlockNumber = response.data.blobs[0].blockNumber;
const startBlock = Math.floor(timestamp / 12); // Approximate block number based on 12s block time

for (
let blockNumber = startBlock;
blockNumber <= currentBlockNumber;
blockNumber++
) {
try {
console.log('Indexing blob data from block', blockNumber);
await this.storeBlockPrice(blockNumber, resource);
} catch (error) {
console.error(`Error processing block ${blockNumber}:`, error);
Sentry.withScope((scope) => {
scope.setExtra('blockNumber', blockNumber);
scope.setExtra('resource', resource.slug);
scope.setExtra('timestamp', timestamp);
Sentry.captureException(error);
});
}
}
return true;
} catch (error) {
console.error('Failed to index blocks from timestamp:', error);
return false;
}
}

async indexBlocks(resource: Resource, blocks: number[]): Promise<boolean> {
for (const blockNumber of blocks) {
try {
console.log('Indexing blob data from block', blockNumber);
await this.storeBlockPrice(blockNumber, resource);
} catch (error) {
console.error(`Error processing block ${blockNumber}:`, error);
Sentry.withScope((scope) => {
scope.setExtra('blockNumber', blockNumber);
scope.setExtra('resource', resource.slug);
Sentry.captureException(error);
});
}
}
return true;
}

async watchBlocksForResource(resource: Resource) {
if (this.isWatching) {
console.log(
'[EthBlobIndexer] Already watching blocks for resource:',
resource.slug
);
return;
}

console.log(
'[EthBlobIndexer] Starting blob watcher for resource:',
resource.slug
);
this.isWatching = true;
let lastProcessedBlock = 0;

const pollNewBlocks = async () => {
try {
console.log('[EthBlobIndexer] Polling for new blocks...');

// Get most recent block
const response = await axios.get(`${this.blobscanApiUrl}/blocks`, {
params: {
ps: 1,
sort: 'desc',
},
});

if (!response.data?.blocks?.[0]) {
console.log('[EthBlobIndexer] No new blocks found in this poll');
return;
}

const latestBlock = response.data.blocks[0];
const currentBlockNumber = latestBlock.number;

if (currentBlockNumber > lastProcessedBlock) {
console.log(`[EthBlobIndexer] Found new block ${currentBlockNumber}`);

try {
await this.storeBlockPrice(currentBlockNumber, resource);
const blobData =
await this.fetchBlobDataFromBlobscan(currentBlockNumber);
if (blobData) {
console.log(
`[EthBlobIndexer] Successfully processed block ${currentBlockNumber}:`,
{
blockNumber: currentBlockNumber,
timestamp: new Date(blobData.timestamp * 1000).toISOString(),
blobGasPrice: `${this.formatGwei(blobData.blobGasPrice)} Gwei (${blobData.blobGasPrice} wei)`,
blobGasUsed: blobData.blobGasUsed,
feePaid: (
BigInt(blobData.blobGasPrice) * BigInt(blobData.blobGasUsed)
).toString(),
}
);
}
lastProcessedBlock = currentBlockNumber;
} catch (error) {
console.error(
`[EthBlobIndexer] Failed to process block ${currentBlockNumber}:`,
error
);
Sentry.captureException(error);
}
} else {
console.log('[EthBlobIndexer] No new blocks to process');
}
} catch (error) {
console.error('[EthBlobIndexer] Error polling new blocks:', error);
if (axios.isAxiosError(error)) {
console.error('[EthBlobIndexer] API Error details:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
});
}
Sentry.captureException(error);
}

// Poll every 12 seconds (average Ethereum block time)
setTimeout(pollNewBlocks, 12000);
};

console.log(
'[EthBlobIndexer] Starting initial poll for resource:',
resource.slug
);
// Start polling
pollNewBlocks();
}
}

export default ethBlobsIndexer;
16 changes: 16 additions & 0 deletions packages/app/public/eth-blob.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 1 addition & 11 deletions packages/app/public/eth.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/app/src/components/DepthChart/usePoolData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type AbiFunction } from 'viem';
import { useReadContracts } from 'wagmi';

import { useFoil } from '../../lib/context/FoilProvider';
import { TICK_SPACING_DEFAULT } from '~/lib/constants/constants';
import { TICK_SPACING_DEFAULT } from '~/lib/constants';
import { PeriodContext } from '~/lib/context/PeriodProvider';
import type { GraphTick, PoolData } from '~/lib/utils/liquidityUtil';
import { getFullPool } from '~/lib/utils/liquidityUtil';
Expand Down
Loading

0 comments on commit 5150051

Please sign in to comment.