Refactor PoolsCache (part 2) [TKR-500] (#526)
* Introduce NoOpPoolsCache and use it in unsupported chains for BeethovenX * Use `NoOpPoolsCache` for `CreamPoolsCache` and `BalancerPoolsCache` on unsupported chains
This commit is contained in:
parent
14dcee5bb6
commit
ab7dc33ca4
@ -2176,7 +2176,6 @@ export const LIDO_INFO_BY_CHAIN = valueByChainId<LidoInfo>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer';
|
|
||||||
export const BALANCER_TOP_POOLS_FETCHED = 250;
|
export const BALANCER_TOP_POOLS_FETCHED = 250;
|
||||||
export const BALANCER_MAX_POOLS_FETCHED = 3;
|
export const BALANCER_MAX_POOLS_FETCHED = 3;
|
||||||
|
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
|
import { ChainId } from '@0x/contract-addresses';
|
||||||
import { getPoolsWithTokens, parsePoolData } from 'balancer-labs-sor-v1';
|
import { getPoolsWithTokens, parsePoolData } from 'balancer-labs-sor-v1';
|
||||||
import { Pool } from 'balancer-labs-sor-v1/dist/types';
|
import { Pool } from 'balancer-labs-sor-v1/dist/types';
|
||||||
import { gql, request } from 'graphql-request';
|
import { gql, request } from 'graphql-request';
|
||||||
|
|
||||||
import { DEFAULT_WARNING_LOGGER } from '../../../constants';
|
import { DEFAULT_WARNING_LOGGER } from '../../../constants';
|
||||||
import { LogFunction } from '../../../types';
|
import { LogFunction } from '../../../types';
|
||||||
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_SUBGRAPH_URL, BALANCER_TOP_POOLS_FETCHED } from '../constants';
|
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_TOP_POOLS_FETCHED } from '../constants';
|
||||||
|
|
||||||
import { CacheValue, PoolsCache } from './pools_cache';
|
import { NoOpPoolsCache } from './no_op_pools_cache';
|
||||||
|
import { AbstractPoolsCache, CacheValue, PoolsCache } from './pools_cache';
|
||||||
|
|
||||||
// tslint:disable:custom-no-magic-numbers
|
// tslint:disable:custom-no-magic-numbers
|
||||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
// tslint:enable:custom-no-magic-numbers
|
// tslint:enable:custom-no-magic-numbers
|
||||||
|
// tslint:disable: member-ordering
|
||||||
|
|
||||||
|
const BALANCER_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer';
|
||||||
|
|
||||||
interface BalancerPoolResponse {
|
interface BalancerPoolResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@ -20,8 +25,16 @@ interface BalancerPoolResponse {
|
|||||||
totalWeight: string;
|
totalWeight: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BalancerPoolsCache extends PoolsCache {
|
export class BalancerPoolsCache extends AbstractPoolsCache {
|
||||||
constructor(
|
public static create(chainId: ChainId): PoolsCache {
|
||||||
|
if (chainId !== ChainId.Mainnet) {
|
||||||
|
return new NoOpPoolsCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BalancerPoolsCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(
|
||||||
private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL,
|
private readonly _subgraphUrl: string = BALANCER_SUBGRAPH_URL,
|
||||||
cache: Map<string, CacheValue> = new Map(),
|
cache: Map<string, CacheValue> = new Map(),
|
||||||
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
|
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
|
||||||
|
@ -9,7 +9,10 @@ import { LogFunction } from '../../../types';
|
|||||||
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_TOP_POOLS_FETCHED } from '../constants';
|
import { BALANCER_MAX_POOLS_FETCHED, BALANCER_TOP_POOLS_FETCHED } from '../constants';
|
||||||
|
|
||||||
import { parsePoolData } from './balancer_sor_v2';
|
import { parsePoolData } from './balancer_sor_v2';
|
||||||
import { CacheValue, PoolsCache } from './pools_cache';
|
import { NoOpPoolsCache } from './no_op_pools_cache';
|
||||||
|
import { AbstractPoolsCache, CacheValue, PoolsCache } from './pools_cache';
|
||||||
|
|
||||||
|
// tslint:disable: member-ordering
|
||||||
|
|
||||||
const BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN = new Map<ChainId, string>([
|
const BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN = new Map<ChainId, string>([
|
||||||
[ChainId.Fantom, 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx'],
|
[ChainId.Fantom, 'https://api.thegraph.com/subgraphs/name/beethovenxfi/beethovenx'],
|
||||||
@ -28,11 +31,11 @@ interface BalancerPoolResponse {
|
|||||||
amp: string | null;
|
amp: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BalancerV2PoolsCache extends PoolsCache {
|
export class BalancerV2PoolsCache extends AbstractPoolsCache {
|
||||||
public static createBeethovenXPoolCache(chainId: ChainId): BalancerV2PoolsCache | undefined {
|
public static createBeethovenXPoolCache(chainId: ChainId): PoolsCache {
|
||||||
const subgraphUrl = BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN.get(chainId);
|
const subgraphUrl = BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN.get(chainId);
|
||||||
if (subgraphUrl === undefined) {
|
if (subgraphUrl === undefined) {
|
||||||
return undefined;
|
return new NoOpPoolsCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BalancerV2PoolsCache(subgraphUrl);
|
return new BalancerV2PoolsCache(subgraphUrl);
|
||||||
@ -58,7 +61,7 @@ export class BalancerV2PoolsCache extends PoolsCache {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
private constructor(
|
||||||
private readonly subgraphUrl: string,
|
private readonly subgraphUrl: string,
|
||||||
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
|
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
|
||||||
private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
|
private readonly _topPoolsFetched: number = BALANCER_TOP_POOLS_FETCHED,
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
|
import { ChainId } from '@0x/contract-addresses';
|
||||||
import { Pool } from 'balancer-labs-sor-v1/dist/types';
|
import { Pool } from 'balancer-labs-sor-v1/dist/types';
|
||||||
import { getPoolsWithTokens, parsePoolData } from 'cream-sor';
|
import { getPoolsWithTokens, parsePoolData } from 'cream-sor';
|
||||||
|
|
||||||
import { BALANCER_MAX_POOLS_FETCHED } from '../constants';
|
import { BALANCER_MAX_POOLS_FETCHED } from '../constants';
|
||||||
|
|
||||||
import { CacheValue, PoolsCache } from './pools_cache';
|
import { NoOpPoolsCache } from './no_op_pools_cache';
|
||||||
|
import { AbstractPoolsCache, CacheValue, PoolsCache } from './pools_cache';
|
||||||
|
|
||||||
export class CreamPoolsCache extends PoolsCache {
|
export class CreamPoolsCache extends AbstractPoolsCache {
|
||||||
constructor(
|
public static create(chainId: ChainId): PoolsCache {
|
||||||
_cache: Map<string, CacheValue> = new Map(),
|
if (chainId !== ChainId.Mainnet) {
|
||||||
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
|
return new NoOpPoolsCache();
|
||||||
) {
|
}
|
||||||
super(_cache);
|
|
||||||
|
return new CreamPoolsCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
|
protected async _fetchPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
|
||||||
try {
|
try {
|
||||||
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
|
const poolData = (await getPoolsWithTokens(takerToken, makerToken)).pools;
|
||||||
@ -25,4 +27,10 @@ export class CreamPoolsCache extends PoolsCache {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private constructor(
|
||||||
|
_cache: Map<string, CacheValue> = new Map(),
|
||||||
|
private readonly maxPoolsFetched: number = BALANCER_MAX_POOLS_FETCHED,
|
||||||
|
) {
|
||||||
|
super(_cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export { BalancerPoolsCache } from './balancer_utils';
|
export { BalancerPoolsCache } from './balancer_utils';
|
||||||
export { BalancerV2PoolsCache } from './balancer_v2_utils';
|
export { BalancerV2PoolsCache } from './balancer_v2_utils';
|
||||||
export { CreamPoolsCache } from './cream_utils';
|
export { CreamPoolsCache } from './cream_utils';
|
||||||
export { PoolsCache } from './pools_cache';
|
export { AbstractPoolsCache, PoolsCache } from './pools_cache';
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { Pool, PoolsCache } from './pools_cache';
|
||||||
|
|
||||||
|
// tslint:disable:prefer-function-over-method
|
||||||
|
|
||||||
|
export class NoOpPoolsCache implements PoolsCache {
|
||||||
|
public async getFreshPoolsForPairAsync(
|
||||||
|
_takerToken: string,
|
||||||
|
_makerToken: string,
|
||||||
|
_timeoutMs?: number | undefined,
|
||||||
|
): Promise<Pool[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPoolAddressesForPair(_takerToken: string, _makerToken: string): string[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public isFresh(_takerToken: string, _makerToken: string): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,13 @@ const DEFAULT_CACHE_TIME_MS = (ONE_HOUR_IN_SECONDS / 2) * ONE_SECOND_MS;
|
|||||||
const DEFAULT_TIMEOUT_MS = 3000;
|
const DEFAULT_TIMEOUT_MS = 3000;
|
||||||
// tslint:enable:custom-no-magic-numbers
|
// tslint:enable:custom-no-magic-numbers
|
||||||
|
|
||||||
export abstract class PoolsCache {
|
export interface PoolsCache {
|
||||||
|
getFreshPoolsForPairAsync(takerToken: string, makerToken: string, timeoutMs?: number): Promise<Pool[]>;
|
||||||
|
getPoolAddressesForPair(takerToken: string, makerToken: string): string[];
|
||||||
|
isFresh(takerToken: string, makerToken: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class AbstractPoolsCache implements PoolsCache {
|
||||||
protected static _getKey(takerToken: string, makerToken: string): string {
|
protected static _getKey(takerToken: string, makerToken: string): string {
|
||||||
return `${takerToken}-${makerToken}`;
|
return `${takerToken}-${makerToken}`;
|
||||||
}
|
}
|
||||||
@ -51,18 +57,18 @@ export abstract class PoolsCache {
|
|||||||
|
|
||||||
public isFresh(takerToken: string, makerToken: string): boolean {
|
public isFresh(takerToken: string, makerToken: string): boolean {
|
||||||
const value = this._getValue(takerToken, makerToken);
|
const value = this._getValue(takerToken, makerToken);
|
||||||
return !PoolsCache._isExpired(value);
|
return !AbstractPoolsCache._isExpired(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getValue(takerToken: string, makerToken: string): CacheValue | undefined {
|
protected _getValue(takerToken: string, makerToken: string): CacheValue | undefined {
|
||||||
const key = PoolsCache._getKey(takerToken, makerToken);
|
const key = AbstractPoolsCache._getKey(takerToken, makerToken);
|
||||||
return this._cache.get(key);
|
return this._cache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _getAndSaveFreshPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
|
protected async _getAndSaveFreshPoolsForPairAsync(takerToken: string, makerToken: string): Promise<Pool[]> {
|
||||||
const key = PoolsCache._getKey(takerToken, makerToken);
|
const key = AbstractPoolsCache._getKey(takerToken, makerToken);
|
||||||
const value = this._cache.get(key);
|
const value = this._cache.get(key);
|
||||||
if (!PoolsCache._isExpired(value)) {
|
if (!AbstractPoolsCache._isExpired(value)) {
|
||||||
return value!.pools;
|
return value!.pools;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +79,7 @@ export abstract class PoolsCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[], expiresAt: number): void {
|
protected _cachePoolsForPair(takerToken: string, makerToken: string, pools: Pool[], expiresAt: number): void {
|
||||||
const key = PoolsCache._getKey(takerToken, makerToken);
|
const key = AbstractPoolsCache._getKey(takerToken, makerToken);
|
||||||
this._cache.set(key, { pools, expiresAt });
|
this._cache.set(key, { pools, expiresAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ import {
|
|||||||
} from './constants';
|
} from './constants';
|
||||||
import { getGeistInfoForPair } from './geist_utils';
|
import { getGeistInfoForPair } from './geist_utils';
|
||||||
import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
|
import { getLiquidityProvidersForPair } from './liquidity_provider_utils';
|
||||||
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache } from './pools_cache';
|
import { BalancerPoolsCache, BalancerV2PoolsCache, CreamPoolsCache, PoolsCache } from './pools_cache';
|
||||||
import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new';
|
import { BalancerV2SwapInfoCache } from './pools_cache/balancer_v2_utils_new';
|
||||||
import { SamplerContractOperation } from './sampler_contract_operation';
|
import { SamplerContractOperation } from './sampler_contract_operation';
|
||||||
import { SamplerNoOperation } from './sampler_no_operation';
|
import { SamplerNoOperation } from './sampler_no_operation';
|
||||||
@ -114,10 +114,10 @@ export const TWO_HOP_SOURCE_FILTERS = SourceFilters.all().exclude([
|
|||||||
export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]);
|
export const BATCH_SOURCE_FILTERS = SourceFilters.all().exclude([ERC20BridgeSource.MultiHop, ERC20BridgeSource.Native]);
|
||||||
|
|
||||||
export interface PoolsCacheMap {
|
export interface PoolsCacheMap {
|
||||||
[ERC20BridgeSource.Balancer]: BalancerPoolsCache;
|
[ERC20BridgeSource.Balancer]: PoolsCache;
|
||||||
[ERC20BridgeSource.BalancerV2]: BalancerV2SwapInfoCache | undefined;
|
[ERC20BridgeSource.BalancerV2]: BalancerV2SwapInfoCache | undefined;
|
||||||
[ERC20BridgeSource.Beethovenx]: BalancerV2PoolsCache | undefined;
|
[ERC20BridgeSource.Beethovenx]: PoolsCache;
|
||||||
[ERC20BridgeSource.Cream]: CreamPoolsCache;
|
[ERC20BridgeSource.Cream]: PoolsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable:no-inferred-empty-object-type no-unbound-method
|
// tslint:disable:no-inferred-empty-object-type no-unbound-method
|
||||||
@ -156,8 +156,8 @@ export class SamplerOperations {
|
|||||||
? poolsCaches
|
? poolsCaches
|
||||||
: {
|
: {
|
||||||
[ERC20BridgeSource.Beethovenx]: BalancerV2PoolsCache.createBeethovenXPoolCache(chainId),
|
[ERC20BridgeSource.Beethovenx]: BalancerV2PoolsCache.createBeethovenXPoolCache(chainId),
|
||||||
[ERC20BridgeSource.Balancer]: new BalancerPoolsCache(),
|
[ERC20BridgeSource.Balancer]: BalancerPoolsCache.create(chainId),
|
||||||
[ERC20BridgeSource.Cream]: new CreamPoolsCache(),
|
[ERC20BridgeSource.Cream]: CreamPoolsCache.create(chainId),
|
||||||
[ERC20BridgeSource.BalancerV2]:
|
[ERC20BridgeSource.BalancerV2]:
|
||||||
BALANCER_V2_VAULT_ADDRESS_BY_CHAIN[chainId] === NULL_ADDRESS
|
BALANCER_V2_VAULT_ADDRESS_BY_CHAIN[chainId] === NULL_ADDRESS
|
||||||
? undefined
|
? undefined
|
||||||
@ -1621,9 +1621,6 @@ export class SamplerOperations {
|
|||||||
}
|
}
|
||||||
case ERC20BridgeSource.Beethovenx: {
|
case ERC20BridgeSource.Beethovenx: {
|
||||||
const cache = this.poolsCaches[source];
|
const cache = this.poolsCaches[source];
|
||||||
if (cache === undefined) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const poolAddresses = cache.getPoolAddressesForPair(takerToken, makerToken);
|
const poolAddresses = cache.getPoolAddressesForPair(takerToken, makerToken);
|
||||||
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
|
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
|
||||||
if (vault === NULL_ADDRESS) {
|
if (vault === NULL_ADDRESS) {
|
||||||
@ -1976,9 +1973,6 @@ export class SamplerOperations {
|
|||||||
}
|
}
|
||||||
case ERC20BridgeSource.Beethovenx: {
|
case ERC20BridgeSource.Beethovenx: {
|
||||||
const cache = this.poolsCaches[source];
|
const cache = this.poolsCaches[source];
|
||||||
if (cache === undefined) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const poolIds = cache.getPoolAddressesForPair(takerToken, makerToken) || [];
|
const poolIds = cache.getPoolAddressesForPair(takerToken, makerToken) || [];
|
||||||
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
|
const vault = BEETHOVEN_X_VAULT_ADDRESS_BY_CHAIN[this.chainId];
|
||||||
if (vault === NULL_ADDRESS) {
|
if (vault === NULL_ADDRESS) {
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
SOURCE_FLAGS,
|
SOURCE_FLAGS,
|
||||||
ZERO_AMOUNT,
|
ZERO_AMOUNT,
|
||||||
} from '../src/utils/market_operation_utils/constants';
|
} from '../src/utils/market_operation_utils/constants';
|
||||||
import { PoolsCache } from '../src/utils/market_operation_utils/pools_cache';
|
import { AbstractPoolsCache } from '../src/utils/market_operation_utils/pools_cache';
|
||||||
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
||||||
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
|
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
|
||||||
import { SourceFilters } from '../src/utils/market_operation_utils/source_filters';
|
import { SourceFilters } from '../src/utils/market_operation_utils/source_filters';
|
||||||
@ -98,7 +98,7 @@ async function getMarketBuyOrdersAsync(
|
|||||||
return utils.getOptimizerResultAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts);
|
return utils.getOptimizerResultAsync(nativeOrders, makerAmount, MarketOperation.Buy, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockPoolsCache extends PoolsCache {
|
class MockPoolsCache extends AbstractPoolsCache {
|
||||||
constructor(private readonly _handler: (takerToken: string, makerToken: string) => Pool[]) {
|
constructor(private readonly _handler: (takerToken: string, makerToken: string) => Pool[]) {
|
||||||
super(new Map());
|
super(new Map());
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ describe('Pools Caches for Balancer-based sampling', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('BalancerPoolsCache', () => {
|
describe('BalancerPoolsCache', () => {
|
||||||
const cache = new BalancerPoolsCache();
|
const cache = BalancerPoolsCache.create(ChainId.Mainnet);
|
||||||
it('fetches pools', async () => {
|
it('fetches pools', async () => {
|
||||||
const pairs = [
|
const pairs = [
|
||||||
[usdcAddress, daiAddress],
|
[usdcAddress, daiAddress],
|
||||||
@ -58,15 +58,14 @@ describe('Pools Caches for Balancer-based sampling', () => {
|
|||||||
[wftmAddress, fantomWethAddress],
|
[wftmAddress, fantomWethAddress],
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(cache).not.null();
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pairs.map(async ([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache!, takerToken, makerToken)),
|
pairs.map(async ([takerToken, makerToken]) => fetchAndAssertPoolsAsync(cache, takerToken, makerToken)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CreamPoolsCache', () => {
|
describe('CreamPoolsCache', () => {
|
||||||
const cache = new CreamPoolsCache();
|
const cache = CreamPoolsCache.create(ChainId.Mainnet);
|
||||||
it('fetches pools', async () => {
|
it('fetches pools', async () => {
|
||||||
const pairs = [
|
const pairs = [
|
||||||
[usdcAddress, creamAddress],
|
[usdcAddress, creamAddress],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user