Files
protocol/apps-node/rfq-api/test/utils/cache_service_test.ts

271 lines
11 KiB
TypeScript

import { ChainId } from '@0x/contract-addresses';
import { BigNumber } from '@0x/utils';
import Redis from 'ioredis';
import { ONE_MINUTE_MS } from '../../src/core/constants';
import { ERC20Owner } from '../../src/core/types';
import { CacheClient } from '../../src/utils/cache_client';
import { REDIS_PORT } from '../constants';
import { setupDependenciesAsync, TeardownDependenciesFunctionHandle } from '../test_utils/deployment';
jest.setTimeout(ONE_MINUTE_MS * 2);
let teardownDependencies: TeardownDependenciesFunctionHandle;
describe('CacheClient', () => {
let redis: Redis;
let cacheClient: CacheClient;
const chainId = ChainId.Ganache;
const makerA = '0x1111111111111111111111111111111111111111';
const makerB = '0x2222222222222222222222222222222222222222';
const makerC = '0x3333333333333333333333333333333333333333';
const tokenA = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const tokenB = '0x6B175474E89094C44Da98b954EedeAC495271d0F';
const tokenC = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
// compareFn is used to determine the order of ERC20Owner elements
const compareFn = (a: ERC20Owner, b: ERC20Owner) => a.owner.localeCompare(b.owner);
beforeAll(async () => {
teardownDependencies = await setupDependenciesAsync(['redis']);
redis = new Redis(REDIS_PORT);
cacheClient = new CacheClient(redis);
});
afterAll(async () => {
await cacheClient.closeAsync();
if (!teardownDependencies()) {
throw new Error('Failed to tear down dependencies');
}
});
afterEach(async () => {
await redis.flushdb();
});
describe('addERC20OwnerAsync', () => {
it('adds pending address to observed addresses without error', async () => {
expect(cacheClient.addERC20OwnerAsync(chainId, { owner: makerA, token: tokenA })).resolves.toEqual(void 0);
});
});
describe('getERC20OwnersAsync', () => {
const addresses = [
{ owner: makerA, token: tokenA },
{ owner: makerB, token: tokenB },
{ owner: makerC, token: tokenC },
];
it('fetches maker token addresses in correct format', async () => {
addresses.forEach(async (address) => {
await cacheClient.addERC20OwnerAsync(chainId, address);
});
const cachedAddresses = await cacheClient.getERC20OwnersAsync(chainId);
expect(cachedAddresses.sort(compareFn)).toEqual(addresses.sort(compareFn));
});
it('fetches empty arrays if no addresses are found in the set', async () => {
expect(await cacheClient.getERC20OwnersAsync(chainId)).toEqual([]);
});
});
describe('setERC20OwnerBalancesAsync', () => {
const addresses = [
{ owner: makerA, token: tokenA },
{ owner: makerB, token: tokenB },
{ owner: makerC, token: tokenC },
];
const balances = [new BigNumber(1), new BigNumber(2), new BigNumber(3)];
it('sets balances in the cache without error', async () => {
expect(cacheClient.setERC20OwnerBalancesAsync(chainId, addresses, balances)).resolves.toEqual(void 0);
});
it('should fail when addresses do not match balances', async () => {
expect(cacheClient.setERC20OwnerBalancesAsync(chainId, addresses, balances.slice(0, -1))).rejects.toThrow(
'balances',
);
});
it('should not fail when addresses are empty', async () => {
expect(cacheClient.setERC20OwnerBalancesAsync(chainId, [], [])).resolves.toEqual(void 0);
});
});
describe('getERC20OwnerBalancesAsync', () => {
const addresses = [
{ owner: makerA, token: tokenA },
{ owner: makerB, token: tokenB },
{ owner: makerC, token: tokenC },
];
const balances = [new BigNumber(1), new BigNumber(2), new BigNumber(3)];
beforeEach(async () => {
await cacheClient.setERC20OwnerBalancesAsync(chainId, addresses, balances);
});
it('fetches correct balances from the cache', async () => {
expect(await cacheClient.getERC20OwnerBalancesAsync(chainId, addresses)).toEqual([
new BigNumber(1),
new BigNumber(2),
new BigNumber(3),
]);
});
it('returns null balances if addresses are not in the cache', async () => {
const badAddresses = [
{ owner: makerA, token: tokenA },
{ owner: makerB, token: tokenB },
{ owner: '0x0000000000000000000000000000000000000000', token: tokenC },
];
expect(await cacheClient.getERC20OwnerBalancesAsync(chainId, badAddresses)).toEqual([
new BigNumber(1),
new BigNumber(2),
null,
]);
});
it('returns null balances if addresses from a different chain are supplied', async () => {
expect(await cacheClient.getERC20OwnerBalancesAsync(ChainId.PolygonMumbai, addresses)).toEqual([
null,
null,
null,
]);
});
it('returns an empty array if addresses are empty', async () => {
expect(await cacheClient.getERC20OwnerBalancesAsync(chainId, [])).toEqual([]);
});
});
describe('evictZeroBalancesAsync', () => {
const addresses = [
{ owner: makerA, token: tokenA },
{ owner: makerB, token: tokenB },
{ owner: makerC, token: tokenC },
];
it('evicts zeroed entries from the cache', async () => {
addresses.forEach(async (address) => {
await cacheClient.addERC20OwnerAsync(chainId, address);
});
let cachedAddresses = await cacheClient.getERC20OwnersAsync(chainId);
expect(cachedAddresses.sort(compareFn)).toEqual(addresses.sort(compareFn));
const balances = [new BigNumber(1), new BigNumber(2), new BigNumber(0)];
await cacheClient.setERC20OwnerBalancesAsync(chainId, addresses, balances);
const numEvicted = await cacheClient.evictZeroBalancesAsync(chainId);
expect(numEvicted).toEqual(1);
cachedAddresses = await cacheClient.getERC20OwnersAsync(chainId);
expect(cachedAddresses.sort(compareFn)).toEqual(addresses.slice(0, 2).sort(compareFn));
});
it('does not evict any entries if there are no stale balances', async () => {
addresses.forEach(async (address) => {
await cacheClient.addERC20OwnerAsync(chainId, address);
});
let cachedAddresses = await cacheClient.getERC20OwnersAsync(chainId);
expect(cachedAddresses.sort(compareFn)).toEqual(addresses.sort(compareFn));
const balances = [new BigNumber(1), new BigNumber(2), new BigNumber(3)];
await cacheClient.setERC20OwnerBalancesAsync(chainId, addresses, balances);
const numEvicted = await cacheClient.evictZeroBalancesAsync(chainId);
expect(numEvicted).toEqual(0);
cachedAddresses = await cacheClient.getERC20OwnersAsync(chainId);
expect(cachedAddresses.sort(compareFn)).toEqual(addresses.sort(compareFn));
});
it('does not error if the address set is empty', async () => {
const owners = await cacheClient.getERC20OwnersAsync(chainId);
expect(owners.length).toEqual(0);
const numEvicted = await cacheClient.evictZeroBalancesAsync(chainId);
expect(numEvicted).toEqual(0);
});
});
describe('coolDownMakerForPair', () => {
const makerId1 = 'makerId1';
const takerToken = 'takerToken';
const makerToken = 'makerToken';
it('should add new makers to the cooling down set for a pair', async () => {
const isUpdated = await cacheClient.addMakerToCooldownAsync(
makerId1,
Date.now(),
chainId,
takerToken,
makerToken,
);
expect(isUpdated).toEqual(true);
});
it('should update endTime to a time later than existing endTime', async () => {
const now = Date.now();
const oneMinuteLater = now + ONE_MINUTE_MS;
await cacheClient.addMakerToCooldownAsync(makerId1, now, chainId, takerToken, makerToken);
const isUpdated = await cacheClient.addMakerToCooldownAsync(
makerId1,
oneMinuteLater,
chainId,
makerToken,
takerToken,
);
expect(isUpdated).toEqual(true);
});
it('should not update endTime to a time earlier than existing endTime', async () => {
const now = Date.now();
const oneMinuteEarlier = now - ONE_MINUTE_MS;
await cacheClient.addMakerToCooldownAsync(makerId1, now, chainId, takerToken, makerToken);
const isUpdated = await cacheClient.addMakerToCooldownAsync(
makerId1,
oneMinuteEarlier,
chainId,
takerToken,
makerToken,
);
expect(isUpdated).toEqual(false);
});
});
describe('getCoolingDownMakersForPair', () => {
const makerId1 = 'makerId1';
const makerId2 = 'makerId2';
const takerToken = 'takerToken';
const otherTakerToken = 'otherTakerToken';
const makerToken = 'makerToken';
it('should get all makers that are cooling down', async () => {
const now = Date.now();
const oneMinuteLater = now + ONE_MINUTE_MS;
await cacheClient.addMakerToCooldownAsync(makerId1, oneMinuteLater, chainId, takerToken, makerToken);
await cacheClient.addMakerToCooldownAsync(makerId2, oneMinuteLater, chainId, takerToken, makerToken);
const result = await cacheClient.getMakersInCooldownForPairAsync(chainId, takerToken, makerToken, now);
expect(result).toEqual([makerId1, makerId2]);
});
it('should not include makers whose cooling down periods already ended', async () => {
const now = Date.now();
const oneMinuteEarlier = now - ONE_MINUTE_MS;
const oneMinuteLater = now + ONE_MINUTE_MS;
await cacheClient.addMakerToCooldownAsync(makerId1, oneMinuteEarlier, chainId, takerToken, makerToken);
await cacheClient.addMakerToCooldownAsync(makerId2, oneMinuteLater, chainId, takerToken, makerToken);
const result = await cacheClient.getMakersInCooldownForPairAsync(chainId, takerToken, makerToken, now);
expect(result).toEqual([makerId2]);
});
it('should only include makers that are cooling down for this pair', async () => {
const now = Date.now();
const oneMinuteLater = now + ONE_MINUTE_MS;
await cacheClient.addMakerToCooldownAsync(makerId1, oneMinuteLater, chainId, takerToken, makerToken);
await cacheClient.addMakerToCooldownAsync(makerId2, oneMinuteLater, chainId, otherTakerToken, makerToken);
const result = await cacheClient.getMakersInCooldownForPairAsync(chainId, takerToken, makerToken, now);
expect(result).toEqual([makerId1]);
});
});
});