-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
web3 integrations + get token info + 24h caching time + unit testings (…
…#73) * Add repo and cached repo for erc20 * Add dependency injection of erc20 repository * Add tests for erc20 cache * Add unit tests for viem * Add JS doc to the new repo * Fix name of test * Add all chains automatically
- Loading branch information
Showing
16 changed files
with
642 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,10 @@ | ||
import { ALL_CHAIN_IDS } from '@cowprotocol/repositories'; | ||
|
||
export const ChainIdSchema = { | ||
title: 'Chain ID', | ||
description: 'Chain ID', | ||
enum: [1, 100, 42161, 11155111], | ||
enum: ALL_CHAIN_IDS, | ||
type: 'integer', | ||
} as const | ||
} as const; | ||
|
||
export const ETHEREUM_ADDRESS_PATTERN = "^0x[a-fA-F0-9]{40}$" | ||
export const ETHEREUM_ADDRESS_PATTERN = '^0x[a-fA-F0-9]{40}$'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { SupportedChainId } from '../types'; | ||
|
||
export const erc20RepositorySymbol = Symbol.for('Erc20Repository'); | ||
|
||
export interface Erc20 { | ||
address: string; | ||
name?: string; | ||
symbol?: string; | ||
decimals?: number; | ||
} | ||
|
||
export interface Erc20Repository { | ||
/** | ||
* Return the ERC20 token information for the given address or null if the token address is not an ERC20 token for the given network. | ||
* @param chainId | ||
* @param tokenAddress | ||
*/ | ||
get(chainId: SupportedChainId, tokenAddress: string): Promise<Erc20 | null>; | ||
} |
133 changes: 133 additions & 0 deletions
133
libs/repositories/src/Erc20Repository/Erc20RepositoryCache.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { Erc20RepositoryCache } from './Erc20RepositoryCache'; | ||
import { Erc20, Erc20Repository } from './Erc20Repository'; | ||
import { CacheRepository } from '../CacheRepository/CacheRepository'; | ||
import { SupportedChainId } from '../types'; | ||
|
||
describe('Erc20RepositoryCache', () => { | ||
let erc20RepositoryCache: Erc20RepositoryCache; | ||
let mockProxy: jest.Mocked<Erc20Repository>; | ||
let mockCache: jest.Mocked<CacheRepository>; | ||
|
||
const chainId: SupportedChainId = 1; | ||
const tokenAddress = '0xTokenAddress'; | ||
const cacheName = 'erc20'; | ||
const cacheTimeSeconds = 60; | ||
const baseCacheKey = `repos:${cacheName}`; | ||
|
||
const erc20Data: Erc20 = { | ||
address: '0x1111111111111111111111111111111111111111', | ||
name: 'Token Name', | ||
symbol: 'TKN', | ||
decimals: 18, | ||
}; | ||
|
||
beforeEach(() => { | ||
mockProxy = { | ||
get: jest.fn(), | ||
} as unknown as jest.Mocked<Erc20Repository>; | ||
|
||
mockCache = { | ||
get: jest.fn(), | ||
set: jest.fn(), | ||
} as unknown as jest.Mocked<CacheRepository>; | ||
|
||
erc20RepositoryCache = new Erc20RepositoryCache( | ||
mockProxy, | ||
mockCache, | ||
cacheName, | ||
cacheTimeSeconds | ||
); | ||
}); | ||
|
||
it('should return cached value if available', async () => { | ||
// GIVEN: Cached value is available | ||
mockCache.get.mockResolvedValue(JSON.stringify(erc20Data)); | ||
|
||
// WHEN: get is called | ||
const result = await erc20RepositoryCache.get(chainId, tokenAddress); | ||
|
||
// THEN: The cache is called | ||
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`; | ||
expect(mockCache.get).toHaveBeenCalledWith(cacheKey); | ||
|
||
// THEN: The proxy is not called | ||
expect(mockProxy.get).not.toHaveBeenCalled(); | ||
|
||
// THEN: The cached value is returned | ||
expect(result).toEqual(erc20Data); | ||
}); | ||
|
||
it('should return null if cached value is NULL_VALUE', async () => { | ||
// GIVEN: Cached value is null | ||
mockCache.get.mockResolvedValue('null'); | ||
|
||
// WHEN: get is called | ||
const result = await erc20RepositoryCache.get(chainId, tokenAddress); | ||
|
||
// THEN: The cache is called | ||
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`; | ||
expect(mockCache.get).toHaveBeenCalledWith(cacheKey); | ||
|
||
// THEN: The proxy is not called | ||
expect(mockProxy.get).not.toHaveBeenCalled(); | ||
|
||
// THEN: null is returned | ||
expect(result).toBeNull(); | ||
}); | ||
|
||
it('should fetch from proxy and cache the result if not cached', async () => { | ||
// GIVEN: Cached value is not available | ||
mockCache.get.mockResolvedValue(null); | ||
|
||
// GIVEN: Proxy returns the value | ||
mockProxy.get.mockResolvedValue(erc20Data); | ||
|
||
// WHEN: get is called | ||
const result = await erc20RepositoryCache.get(chainId, tokenAddress); | ||
|
||
// THEN: The cache is called | ||
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`; | ||
expect(mockCache.get).toHaveBeenCalledWith(cacheKey); | ||
|
||
// THEN: The proxy is called | ||
expect(mockProxy.get).toHaveBeenCalledWith(chainId, tokenAddress); | ||
|
||
// THEN: The value is cached | ||
expect(mockCache.set).toHaveBeenCalledWith( | ||
cacheKey, | ||
JSON.stringify(erc20Data), | ||
cacheTimeSeconds | ||
); | ||
|
||
// THEN: The value is returned | ||
expect(result).toEqual(erc20Data); | ||
}); | ||
|
||
it('should cache NULL_VALUE if proxy returns null', async () => { | ||
// GIVEN: Cached value is not available | ||
mockCache.get.mockResolvedValue(null); | ||
|
||
// GIVEN: Proxy returns null | ||
mockProxy.get.mockResolvedValue(null); | ||
|
||
// WHEN: get is called | ||
const result = await erc20RepositoryCache.get(chainId, tokenAddress); | ||
|
||
// THEN: The cache is called | ||
const cacheKey = `${baseCacheKey}:get:${chainId}:${tokenAddress}`; | ||
expect(mockCache.get).toHaveBeenCalledWith(cacheKey); | ||
|
||
// THEN: The proxy is called | ||
expect(mockProxy.get).toHaveBeenCalledWith(chainId, tokenAddress); | ||
|
||
// THEN: The null value is cached | ||
expect(mockCache.set).toHaveBeenCalledWith( | ||
cacheKey, | ||
'null', | ||
cacheTimeSeconds | ||
); | ||
|
||
// THEN: The result is null | ||
expect(result).toBeNull(); | ||
}); | ||
}); |
43 changes: 43 additions & 0 deletions
43
libs/repositories/src/Erc20Repository/Erc20RepositoryCache.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { inject, injectable } from 'inversify'; | ||
import NodeCache from 'node-cache'; | ||
import { Erc20, Erc20Repository } from './Erc20Repository'; | ||
import { CacheRepository } from '../CacheRepository/CacheRepository'; | ||
import { SupportedChainId } from '../types'; | ||
|
||
const NULL_VALUE = 'null'; | ||
|
||
@injectable() | ||
export class Erc20RepositoryCache implements Erc20Repository { | ||
private baseCacheKey: string; | ||
|
||
constructor( | ||
private proxy: Erc20Repository, | ||
private cache: CacheRepository, | ||
private cacheName: string, | ||
private cacheTimeSeconds: number | ||
) { | ||
this.baseCacheKey = `repos:${this.cacheName}`; | ||
} | ||
|
||
async get( | ||
chainId: SupportedChainId, | ||
tokenAddress: string | ||
): Promise<Erc20 | null> { | ||
const cacheKey = `${this.baseCacheKey}:get:${chainId}:${tokenAddress}`; | ||
|
||
// Get cached value | ||
const valueString = await this.cache.get(cacheKey); | ||
if (valueString) { | ||
return valueString === NULL_VALUE ? null : JSON.parse(valueString); | ||
} | ||
|
||
// Get fresh value from proxy | ||
const value = await this.proxy.get(chainId, tokenAddress); | ||
|
||
// Cache value | ||
const cacheValue = value === null ? NULL_VALUE : JSON.stringify(value); | ||
await this.cache.set(cacheKey, cacheValue, this.cacheTimeSeconds); | ||
|
||
return value; | ||
} | ||
} |
Oops, something went wrong.