Skip to content
Open
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
7 changes: 6 additions & 1 deletion coins/src/getBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getCurrentUnixTimestamp } from "./utils/date";
import genesisBlockTimes from './genesisBlockTimes';
import { sendMessage } from "../../defi/src/utils/discord";
import { DAY } from "./utils/processCoin";
import { createCachedProvider } from "./utils/cachedProvider";

interface TimestampBlock {
height: number;
Expand Down Expand Up @@ -113,7 +114,11 @@ function getExtraProvider(chain: string | undefined) {
return cosmosBlockProvider(chain)
}
if (["lite"].includes(chain as any)) return zkSyncBlockProvider();
return getProvider(chain as any);
const provider = getProvider(chain as any);
if (provider && chain) {
return createCachedProvider(provider, chain);
}
return provider;
}

function isAValidBlockAtThisTimestamp(timestamp: number, chain: string) {
Expand Down
12 changes: 7 additions & 5 deletions coins/src/scripts/checkRpcBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getProvider } from "@defillama/sdk";
import PromisePool from "@supercharge/promise-pool";
import { adaptersRepoChainsJson as chains } from "../getChains";
import { sendMessage } from "../../../defi/src/utils/discord";
import { createCachedProvider } from "../utils/cachedProvider";

type Rpc = {
rpc: string;
Expand Down Expand Up @@ -30,15 +31,16 @@ async function collectHeights(results: Results) {
await Promise.all(
chains.map(async (chain: string) => {
results[chain] = [];
const provider: any = getProvider(chain);
if (!provider) return;
const baseProvider: any = getProvider(chain);
if (!baseProvider) return;

await PromisePool.for(provider.rpcs)
await PromisePool.for(baseProvider.rpcs)
.withConcurrency(1)
.process(async (rpc: any) => {
let thisProvider = provider;
let thisProvider = baseProvider;
thisProvider.rpcs = [rpc];
const block = await provider.getBlock("latest");
const cachedProvider = createCachedProvider(thisProvider, chain);
const block = await cachedProvider.getBlock("latest");
results[chain].push({ rpc: rpc.url, block: block.number });
});
}),
Expand Down
133 changes: 133 additions & 0 deletions coins/src/utils/cache/blockCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { BlockCache } from './blockCache';

describe('BlockCache', () => {
let cache: BlockCache;

beforeEach(() => {
cache = new BlockCache();
});

describe('Block Number Cache', () => {
it('should cache and retrieve specific block numbers', () => {
const blockData = { number: 12345, timestamp: 1234567890 };

cache.setBlock('ethereum', 12345, blockData);
const retrieved = cache.getBlock('ethereum', 12345);

expect(retrieved).toEqual(blockData);
});

it('should cache blocks for different chains separately', () => {
const ethBlock = { number: 12345, timestamp: 1234567890 };
const bscBlock = { number: 54321, timestamp: 9876543210 };

cache.setBlock('ethereum', 12345, ethBlock);
cache.setBlock('bsc', 54321, bscBlock);

expect(cache.getBlock('ethereum', 12345)).toEqual(ethBlock);
expect(cache.getBlock('bsc', 54321)).toEqual(bscBlock);
});

it('should return undefined for non-existent blocks', () => {
const retrieved = cache.getBlock('ethereum', 99999);
expect(retrieved).toBeUndefined();
});

it('should respect LRU eviction when cache is full', () => {
for (let i = 0; i < 100; i++) {
cache.setBlock('ethereum', i, { number: i, timestamp: 1000 + i });
}

let stats = cache.getStats();
expect(stats.blockNumberCacheSize).toBe(100);

for (let i = 0; i < 100; i++) {
const block = cache.getBlock('ethereum', i);
expect(block).toBeDefined();
expect(block?.number).toBe(i);
}

cache.setBlock('ethereum', 100, { number: 100, timestamp: 1100 });

stats = cache.getStats();
expect(stats.blockNumberCacheSize).toBe(100);
expect(cache.getBlock('ethereum', 0)).toBeUndefined();
expect(cache.getBlock('ethereum', 100)).toBeDefined();

for (let i = 101; i < 151; i++) {
cache.setBlock('ethereum', i, { number: i, timestamp: 1000 + i });
}

stats = cache.getStats();
expect(stats.blockNumberCacheSize).toBe(100);

for (let i = 0; i <= 50; i++) {
expect(cache.getBlock('ethereum', i)).toBeUndefined();
}

for (let i = 51; i < 151; i++) {
const block = cache.getBlock('ethereum', i);
expect(block).toBeDefined();
expect(block?.number).toBe(i);
}
});
});

describe('Latest Block Cache', () => {
it('should cache and retrieve latest blocks per chain', () => {
const latestBlock = { number: 99999, timestamp: 1234567890 };

cache.setBlock('ethereum', 'latest', latestBlock);
const retrieved = cache.getBlock('ethereum', 'latest');

expect(retrieved).toEqual(latestBlock);
});

it('should cache latest blocks for different chains separately', () => {
const ethLatest = { number: 99999, timestamp: 1234567890 };
const bscLatest = { number: 88888, timestamp: 9876543210 };

cache.setBlock('ethereum', 'latest', ethLatest);
cache.setBlock('bsc', 'latest', bscLatest);

expect(cache.getBlock('ethereum', 'latest')).toEqual(ethLatest);
expect(cache.getBlock('bsc', 'latest')).toEqual(bscLatest);
});

it('should update latest block when set multiple times', () => {
const firstBlock = { number: 99999, timestamp: 1234567890 };
const secondBlock = { number: 100000, timestamp: 1234567900 };

cache.setBlock('ethereum', 'latest', firstBlock);
cache.setBlock('ethereum', 'latest', secondBlock);

const retrieved = cache.getBlock('ethereum', 'latest');
expect(retrieved).toEqual(secondBlock);
});
});

describe('Cache Statistics', () => {
it('should return correct cache sizes', () => {
cache.setBlock('ethereum', 12345, { number: 12345, timestamp: 1000 });
cache.setBlock('ethereum', 'latest', { number: 99999, timestamp: 2000 });
cache.setBlock('bsc', 'latest', { number: 88888, timestamp: 3000 });

const stats = cache.getStats();
expect(stats.blockNumberCacheSize).toBe(1);
expect(stats.latestBlockCacheSize).toBe(2);
});
});

describe('Cache Clear', () => {
it('should clear all caches', () => {
cache.setBlock('ethereum', 12345, { number: 12345, timestamp: 1000 });
cache.setBlock('ethereum', 'latest', { number: 99999, timestamp: 2000 });

cache.clearAll();

const stats = cache.getStats();
expect(stats.blockNumberCacheSize).toBe(0);
expect(stats.latestBlockCacheSize).toBe(0);
});
});
});
119 changes: 119 additions & 0 deletions coins/src/utils/cache/blockCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
interface CacheEntry<T> {
value: T;
timestamp: number;
}

interface BlockData {
number: number;
timestamp: number;
[key: string]: any;
}

class LRUCache<K, V> {
private cache: Map<string, CacheEntry<V>>;
private maxSize: number;
private ttlSeconds: number;

constructor(maxSize: number, ttlSeconds: number = 60) {
this.cache = new Map();
this.maxSize = maxSize;
this.ttlSeconds = ttlSeconds;
}

private serializeKey(key: K): string {
return JSON.stringify(key);
}

get(key: K): V | undefined {
const serializedKey = this.serializeKey(key);
const entry = this.cache.get(serializedKey);

if (!entry) {
return undefined;
}

const now = Date.now() / 1000;
if (now - entry.timestamp > this.ttlSeconds) {
this.cache.delete(serializedKey);
return undefined;
}

this.cache.delete(serializedKey);
this.cache.set(serializedKey, entry);

return entry.value;
}

set(key: K, value: V): void {
const serializedKey = this.serializeKey(key);

if (this.cache.has(serializedKey)) {
this.cache.delete(serializedKey);
}

if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}

this.cache.set(serializedKey, {
value,
timestamp: Date.now() / 1000
});
}

clear(): void {
this.cache.clear();
}

size(): number {
return this.cache.size;
}
}

interface BlockNumberCacheKey {
chain: string;
height: number | string;
}

export class BlockCache {
private blockNumberCache: LRUCache<BlockNumberCacheKey, BlockData>;

private latestBlockCache: LRUCache<string, BlockData>;

constructor() {
this.blockNumberCache = new LRUCache<BlockNumberCacheKey, BlockData>(100, 300);

this.latestBlockCache = new LRUCache<string, BlockData>(100, 10);
}

getBlock(chain: string, height: number | string): BlockData | undefined {
if (height === "latest") {
return this.latestBlockCache.get(chain);
}

return this.blockNumberCache.get({ chain, height });
}

setBlock(chain: string, height: number | string, blockData: BlockData): void {
if (height === "latest") {
this.latestBlockCache.set(chain, blockData);
} else {
this.blockNumberCache.set({ chain, height }, blockData);
}
}

clearAll(): void {
this.blockNumberCache.clear();
this.latestBlockCache.clear();
}

getStats(): { blockNumberCacheSize: number; latestBlockCacheSize: number } {
return {
blockNumberCacheSize: this.blockNumberCache.size(),
latestBlockCacheSize: this.latestBlockCache.size()
};
}
}

export const blockCache = new BlockCache();
Loading