diff --git a/src/abi/pancakeswap-v3/PancakeswapV3Factory.abi.json b/src/abi/pancakeswap-v3/PancakeswapV3Factory.abi.json new file mode 100644 index 000000000..aaaddbdab --- /dev/null +++ b/src/abi/pancakeswap-v3/PancakeswapV3Factory.abi.json @@ -0,0 +1,300 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_poolDeployer", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "name": "FeeAmountEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": false, + "internalType": "bool", + "name": "whitelistRequested", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "FeeAmountExtraInfoUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "indexed": false, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lmPoolDeployer", + "type": "address" + } + ], + "name": "SetLmPoolDeployer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "verified", + "type": "bool" + } + ], + "name": "WhiteListAdded", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { + "internalType": "uint128", + "name": "amount0Requested", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1Requested", + "type": "uint128" + } + ], + "name": "collectProtocol", + "outputs": [ + { "internalType": "uint128", "name": "amount0", "type": "uint128" }, + { "internalType": "uint128", "name": "amount1", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "tokenA", "type": "address" }, + { "internalType": "address", "name": "tokenB", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" } + ], + "name": "createPool", + "outputs": [ + { "internalType": "address", "name": "pool", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "int24", "name": "tickSpacing", "type": "int24" } + ], + "name": "enableFeeAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint24", "name": "", "type": "uint24" }], + "name": "feeAmountTickSpacing", + "outputs": [{ "internalType": "int24", "name": "", "type": "int24" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint24", "name": "", "type": "uint24" }], + "name": "feeAmountTickSpacingExtraInfo", + "outputs": [ + { "internalType": "bool", "name": "whitelistRequested", "type": "bool" }, + { "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint24", "name": "", "type": "uint24" } + ], + "name": "getPool", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lmPoolDeployer", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolDeployer", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "bool", "name": "whitelistRequested", "type": "bool" }, + { "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "name": "setFeeAmountExtraInfo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "uint32", "name": "feeProtocol0", "type": "uint32" }, + { "internalType": "uint32", "name": "feeProtocol1", "type": "uint32" } + ], + "name": "setFeeProtocol", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pool", "type": "address" }, + { "internalType": "address", "name": "lmPool", "type": "address" } + ], + "name": "setLmPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_lmPoolDeployer", + "type": "address" + } + ], + "name": "setLmPoolDeployer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_owner", "type": "address" } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "bool", "name": "verified", "type": "bool" } + ], + "name": "setWhiteListAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/dex-helper/dummy-dex-helper.ts b/src/dex-helper/dummy-dex-helper.ts index b1db1cca0..287a76d8e 100644 --- a/src/dex-helper/dummy-dex-helper.ts +++ b/src/dex-helper/dummy-dex-helper.ts @@ -115,6 +115,10 @@ class DummyCache implements ICache { return 0; } + async zrem(key: string, membersKeys: string[]): Promise { + return 0; + } + async zadd(key: string, bulkItemsToAdd: (number | string)[], option?: 'NX') { return 0; } diff --git a/src/dex-helper/icache.ts b/src/dex-helper/icache.ts index edaa1536d..4aa725ee6 100644 --- a/src/dex-helper/icache.ts +++ b/src/dex-helper/icache.ts @@ -46,6 +46,8 @@ export interface ICache { zremrangebyscore(key: string, min: number, max: number): Promise; + zrem(key: string, membersKeys: string[]): Promise; + zscore(setKey: string, key: string): Promise; sismember(setKey: string, key: string): Promise; diff --git a/src/dex/algebra/algebra-factory.ts b/src/dex/algebra/algebra-factory.ts new file mode 100644 index 000000000..1650878ad --- /dev/null +++ b/src/dex/algebra/algebra-factory.ts @@ -0,0 +1,70 @@ +import { Interface } from '@ethersproject/abi'; +import { DeepReadonly } from 'ts-essentials'; +import FactoryABI from '../../abi/algebra/AlgebraFactory-v1_1.abi.json'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, Log, Logger } from '../../types'; +import { LogDescription } from 'ethers/lib/utils'; +import { FactoryState } from './types'; + +export type OnPoolCreatedCallback = ({ + token0, + token1, +}: { + token0: string; + token1: string; +}) => Promise; + +/* + * "Stateless" event subscriber in order to capture "PoolCreated" event on new pools created. + * State is present, but it's a placeholder to actually make the events reach handlers (if there's no previous state - `processBlockLogs` is not called) + */ +export class AlgebraFactory extends StatefulEventSubscriber { + handlers: { + [event: string]: (event: any) => Promise; + } = {}; + + logDecoder: (log: Log) => any; + + public readonly factoryIface = new Interface(FactoryABI); + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + protected readonly factoryAddress: Address, + logger: Logger, + protected readonly onPoolCreated: OnPoolCreatedCallback, + mapKey: string = '', + ) { + super(parentName, `${parentName} Factory`, dexHelper, logger, true, mapKey); + + this.addressesSubscribed = [factoryAddress]; + + this.logDecoder = (log: Log) => this.factoryIface.parseLog(log); + + this.handlers['Pool'] = this.handleNewPool.bind(this); + } + + generateState(): FactoryState { + return {}; + } + + protected async processLog( + _: DeepReadonly, + log: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + await this.handlers[event.name](event); + } + + return {}; + } + + async handleNewPool(event: LogDescription) { + const token0 = event.args.token0; + const token1 = event.args.token1; + + await this.onPoolCreated({ token0, token1 }); + } +} diff --git a/src/dex/algebra/algebra.ts b/src/dex/algebra/algebra.ts index 30f16f34f..726cb669d 100644 --- a/src/dex/algebra/algebra.ts +++ b/src/dex/algebra/algebra.ts @@ -38,14 +38,15 @@ import { import { AlgebraMath } from './lib/AlgebraMath'; import { AlgebraEventPoolV1_1 } from './algebra-pool-v1_1'; import { AlgebraEventPoolV1_9 } from './algebra-pool-v1_9'; +import { AlgebraFactory, OnPoolCreatedCallback } from './algebra-factory'; type PoolPairsInfo = { token0: Address; token1: Address; }; -const ALGEBRA_CLEAN_NOT_EXISTING_POOL_TTL_MS = 3 * 60 * 60 * 1000; // 3 hours -const ALGEBRA_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 30 * 60 * 1000; // Once in 30 minutes +const ALGEBRA_CLEAN_NOT_EXISTING_POOL_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days +const ALGEBRA_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 24 * 60 * 60 * 1000; // Once in a day const ALGEBRA_EFFICIENCY_FACTOR = 3; const ALGEBRA_TICK_GAS_COST = 24_000; // Ceiled const ALGEBRA_TICK_BASE_OVERHEAD = 75_000; @@ -59,6 +60,7 @@ const MAX_STALE_STATE_BLOCK_AGE = { type IAlgebraEventPool = AlgebraEventPoolV1_1 | AlgebraEventPoolV1_9; export class Algebra extends SimpleExchange implements IDex { + private readonly factory: AlgebraFactory; readonly isFeeOnTransferSupported: boolean = false; protected eventPools: Record = {}; @@ -114,6 +116,14 @@ export class Algebra extends SimpleExchange implements IDex { this.AlgebraPoolImplem = config.version === 'v1.1' ? AlgebraEventPoolV1_1 : AlgebraEventPoolV1_9; + + this.factory = new AlgebraFactory( + dexHelper, + dexKey, + this.config.factory, + this.logger, + this.onPoolCreatedDeleteFromNonExistingSet, + ); } getAdapters(side: SwapSide): { name: string; index: number }[] | null { @@ -126,13 +136,13 @@ export class Algebra extends SimpleExchange implements IDex { } async initializePricing(blockNumber: number) { - const cleanNonExistingPoolTTLMs = - this.config.cleanExistingPoolTTLMs || - ALGEBRA_CLEAN_NOT_EXISTING_POOL_TTL_MS; + // Init listening to new pools creation + await this.factory.initialize(blockNumber); if (!this.dexHelper.config.isSlave) { const cleanExpiredNotExistingPoolsKeys = async () => { - const maxTimestamp = Date.now() - cleanNonExistingPoolTTLMs; + const maxTimestamp = + Date.now() - ALGEBRA_CLEAN_NOT_EXISTING_POOL_TTL_MS; await this.dexHelper.cache.zremrangebyscore( this.notExistingPoolSetKey, 0, @@ -147,6 +157,37 @@ export class Algebra extends SimpleExchange implements IDex { } } + /* + * When a non existing pool is queried, it's blacklisted for an arbitrary long period in order to prevent issuing too many rpc calls + * Once the pool is created, it gets immediately flagged + */ + onPoolCreatedDeleteFromNonExistingSet: OnPoolCreatedCallback = async ({ + token0, + token1, + }) => { + const logPrefix = '[Algebra.onPoolCreatedDeleteFromNonExistingSet]'; + const [_token0, _token1] = this._sortTokens(token0, token1); + const poolKey = `${token0}_${token1}`.toLowerCase(); + + // consider doing it only from master pool for less calls to distant cache + + // delete entry locally to let local instance discover the pool + delete this.eventPools[this.getPoolIdentifier(_token0, _token1)]; + + try { + this.logger.info( + `${logPrefix} delete pool from not existing set: ${poolKey}`, + ); + // delete pool record from set + await this.dexHelper.cache.zrem(this.notExistingPoolSetKey, [poolKey]); + } catch (error) { + this.logger.error( + `${logPrefix} failed to delete pool from set: ${poolKey}`, + error, + ); + } + }; + async getPool( srcAddress: Address, destAddress: Address, diff --git a/src/dex/algebra/config.ts b/src/dex/algebra/config.ts index daa761097..5809d2037 100644 --- a/src/dex/algebra/config.ts +++ b/src/dex/algebra/config.ts @@ -83,7 +83,6 @@ export const AlgebraConfig: DexConfigMap = { uniswapMulticall: '0x1F98415757620B543A52E61c46B32eB19261F984', deployer: '0x6dd3fb9653b10e806650f107c3b5a0a6ff974f65', version: 'v1.9', - cleanExistingPoolTTLMs: 20 * 60 * 1000, }, }, }; diff --git a/src/dex/algebra/types.ts b/src/dex/algebra/types.ts index 27ca978e1..ec4227967 100644 --- a/src/dex/algebra/types.ts +++ b/src/dex/algebra/types.ts @@ -51,6 +51,8 @@ export type PoolState_v1_9 = { areTicksCompressed: boolean; }; +export type FactoryState = Record; + export type AlgebraData = { path: { tokenIn: Address; @@ -73,7 +75,6 @@ export type DexParams = { version: 'v1.1' | 'v1.9'; forceRPC?: boolean; forceManualStateGenerate?: boolean; - cleanExistingPoolTTLMs?: number; }; export type IAlgebraPoolState = PoolStateV1_1 | PoolState_v1_9; diff --git a/src/dex/index.ts b/src/dex/index.ts index 389106deb..157b8664c 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -317,8 +317,11 @@ export class DexAdapterService { } doesPreProcessingRequireSequentiality(dexKey: string): boolean { - const dex = this.getDexByKey(dexKey); - - return !!dex.needsSequentialPreprocessing; + try { + const dex = this.getDexByKey(dexKey); + return !!dex.needsSequentialPreprocessing; + } catch (e) { + return false; + } } } diff --git a/src/dex/pancakeswap-v3/pancakeswap-v3-factory.ts b/src/dex/pancakeswap-v3/pancakeswap-v3-factory.ts new file mode 100644 index 000000000..8bfc7a46b --- /dev/null +++ b/src/dex/pancakeswap-v3/pancakeswap-v3-factory.ts @@ -0,0 +1,73 @@ +import { Interface } from '@ethersproject/abi'; +import { DeepReadonly } from 'ts-essentials'; +import FactoryABI from '../../abi/pancakeswap-v3/PancakeswapV3Factory.abi.json'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, Log, Logger } from '../../types'; +import { LogDescription } from 'ethers/lib/utils'; +import { FactoryState } from '../uniswap-v3/types'; + +export type OnPoolCreatedCallback = ({ + token0, + token1, + fee, +}: { + token0: string; + token1: string; + fee: bigint; +}) => Promise; + +/* + * "Stateless" event subscriber in order to capture "PoolCreated" event on new pools created. + * State is present, but it's a placeholder to actually make the events reach handlers (if there's no previous state - `processBlockLogs` is not called) + */ +export class PancakeswapV3Factory extends StatefulEventSubscriber { + handlers: { + [event: string]: (event: any) => Promise; + } = {}; + + logDecoder: (log: Log) => any; + + public readonly factoryIface = new Interface(FactoryABI); + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + protected readonly factoryAddress: Address, + logger: Logger, + protected readonly onPoolCreated: OnPoolCreatedCallback, + mapKey: string = '', + ) { + super(parentName, `${parentName} Factory`, dexHelper, logger, true, mapKey); + + this.addressesSubscribed = [factoryAddress]; + + this.logDecoder = (log: Log) => this.factoryIface.parseLog(log); + + this.handlers['PoolCreated'] = this.handleNewPool.bind(this); + } + + generateState(): FactoryState { + return {}; + } + + protected async processLog( + _: DeepReadonly, + log: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + await this.handlers[event.name](event); + } + + return {}; + } + + async handleNewPool(event: LogDescription) { + const token0 = event.args.token0; + const token1 = event.args.token1; + const fee = event.args.fee; + + await this.onPoolCreated({ token0, token1, fee }); + } +} diff --git a/src/dex/pancakeswap-v3/pancakeswap-v3.ts b/src/dex/pancakeswap-v3/pancakeswap-v3.ts index 41d2de86e..1e4013edd 100644 --- a/src/dex/pancakeswap-v3/pancakeswap-v3.ts +++ b/src/dex/pancakeswap-v3/pancakeswap-v3.ts @@ -56,6 +56,10 @@ import { DEFAULT_ID_ERC20, DEFAULT_ID_ERC20_AS_STRING, } from '../../lib/tokens/types'; +import { + OnPoolCreatedCallback, + PancakeswapV3Factory, +} from './pancakeswap-v3-factory'; type PoolPairsInfo = { token0: Address; @@ -63,14 +67,15 @@ type PoolPairsInfo = { fee: string; }; -const PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_TTL_MS = 60 * 60 * 24 * 1000; // 24 hours -const PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 30 * 60 * 1000; // Once in 30 minutes +const PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days +const PANCAKESWAPV3_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 24 * 60 * 60 * 1000; // Once in a day const PANCAKESWAPV3_QUOTE_GASLIMIT = 200_000; export class PancakeswapV3 extends SimpleExchange implements IDex { + private readonly factory: PancakeswapV3Factory; readonly isFeeOnTransferSupported: boolean = false; readonly eventPools: Record = {}; @@ -117,6 +122,14 @@ export class PancakeswapV3 this.notExistingPoolSetKey = `${CACHE_PREFIX}_${network}_${dexKey}_not_existings_pool_set`.toLowerCase(); + + this.factory = new PancakeswapV3Factory( + dexHelper, + dexKey, + this.config.factory, + this.logger, + this.onPoolCreatedDeleteFromNonExistingSet, + ); } get supportedFees() { @@ -133,6 +146,9 @@ export class PancakeswapV3 } async initializePricing(blockNumber: number) { + // Init listening to new pools creation + await this.factory.initialize(blockNumber); + if (!this.dexHelper.config.isSlave) { const cleanExpiredNotExistingPoolsKeys = async () => { const maxTimestamp = @@ -151,6 +167,38 @@ export class PancakeswapV3 } } + /* + * When a non existing pool is queried, it's blacklisted for an arbitrary long period in order to prevent issuing too many rpc calls + * Once the pool is created, it gets immediately flagged + */ + onPoolCreatedDeleteFromNonExistingSet: OnPoolCreatedCallback = async ({ + token0, + token1, + fee, + }) => { + const logPrefix = '[PancakeV3.onPoolCreatedDeleteFromNonExistingSet]'; + const [_token0, _token1] = this._sortTokens(token0, token1); + const poolKey = `${token0}_${token1}_${fee}`.toLowerCase(); + + // consider doing it only from master pool for less calls to distant cache + + // delete entry locally to let local instance discover the pool + delete this.eventPools[this.getPoolIdentifier(_token0, _token1, fee)]; + + try { + this.logger.info( + `${logPrefix} delete pool from not existing set: ${poolKey}`, + ); + // delete pool record from set + await this.dexHelper.cache.zrem(this.notExistingPoolSetKey, [poolKey]); + } catch (error) { + this.logger.error( + `${logPrefix} failed to delete pool from set :${poolKey}`, + error, + ); + } + }; + async getPool( srcAddress: Address, destAddress: Address, diff --git a/src/dex/uniswap-v3/types.ts b/src/dex/uniswap-v3/types.ts index f34e98867..4d29b5099 100644 --- a/src/dex/uniswap-v3/types.ts +++ b/src/dex/uniswap-v3/types.ts @@ -54,6 +54,8 @@ export type PoolState = { balance1: bigint; }; +export type FactoryState = Record; + export type UniswapV3Data = { path: { tokenIn: Address; diff --git a/src/dex/uniswap-v3/uniswap-v3-factory.ts b/src/dex/uniswap-v3/uniswap-v3-factory.ts new file mode 100644 index 000000000..0f568e7ab --- /dev/null +++ b/src/dex/uniswap-v3/uniswap-v3-factory.ts @@ -0,0 +1,73 @@ +import { Interface } from '@ethersproject/abi'; +import { DeepReadonly } from 'ts-essentials'; +import FactoryABI from '../../abi/uniswap-v3/UniswapV3Factory.abi.json'; +import { IDexHelper } from '../../dex-helper/idex-helper'; +import { StatefulEventSubscriber } from '../../stateful-event-subscriber'; +import { Address, Log, Logger } from '../../types'; +import { LogDescription } from 'ethers/lib/utils'; +import { FactoryState } from './types'; + +export type OnPoolCreatedCallback = ({ + token0, + token1, + fee, +}: { + token0: string; + token1: string; + fee: bigint; +}) => Promise; + +/* + * "Stateless" event subscriber in order to capture "PoolCreated" event on new pools created. + * State is present, but it's a placeholder to actually make the events reach handlers (if there's no previous state - `processBlockLogs` is not called) + */ +export class UniswapV3Factory extends StatefulEventSubscriber { + handlers: { + [event: string]: (event: any) => Promise; + } = {}; + + logDecoder: (log: Log) => any; + + public readonly factoryIface = new Interface(FactoryABI); + + constructor( + readonly dexHelper: IDexHelper, + parentName: string, + protected readonly factoryAddress: Address, + logger: Logger, + protected readonly onPoolCreated: OnPoolCreatedCallback, + mapKey: string = '', + ) { + super(parentName, `${parentName} Factory`, dexHelper, logger, true, mapKey); + + this.addressesSubscribed = [factoryAddress]; + + this.logDecoder = (log: Log) => this.factoryIface.parseLog(log); + + this.handlers['PoolCreated'] = this.handleNewPool.bind(this); + } + + generateState(): FactoryState { + return {}; + } + + protected async processLog( + _: DeepReadonly, + log: Readonly, + ): Promise { + const event = this.logDecoder(log); + if (event.name in this.handlers) { + await this.handlers[event.name](event); + } + + return {}; + } + + async handleNewPool(event: LogDescription) { + const token0 = event.args.token0; + const token1 = event.args.token1; + const fee = event.args.fee; + + await this.onPoolCreated({ token0, token1, fee }); + } +} diff --git a/src/dex/uniswap-v3/uniswap-v3.ts b/src/dex/uniswap-v3/uniswap-v3.ts index 0b2295460..eb7d87b82 100644 --- a/src/dex/uniswap-v3/uniswap-v3.ts +++ b/src/dex/uniswap-v3/uniswap-v3.ts @@ -64,6 +64,7 @@ import { DEFAULT_ID_ERC20_AS_STRING, } from '../../lib/tokens/types'; import { OptimalSwapExchange } from '@paraswap/core'; +import { OnPoolCreatedCallback, UniswapV3Factory } from './uniswap-v3-factory'; type PoolPairsInfo = { token0: Address; @@ -71,14 +72,15 @@ type PoolPairsInfo = { fee: string; }; -const UNISWAPV3_CLEAN_NOT_EXISTING_POOL_TTL_MS = 60 * 60 * 24 * 1000; // 24 hours -const UNISWAPV3_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 30 * 60 * 1000; // Once in 30 minutes +const UNISWAPV3_CLEAN_NOT_EXISTING_POOL_TTL_MS = 3 * 24 * 60 * 60 * 1000; // 3 days +const UNISWAPV3_CLEAN_NOT_EXISTING_POOL_INTERVAL_MS = 24 * 60 * 60 * 1000; // Once in a day const UNISWAPV3_QUOTE_GASLIMIT = 200_000; export class UniswapV3 extends SimpleExchange implements IDex { + private readonly factory: UniswapV3Factory; readonly isFeeOnTransferSupported: boolean = false; readonly eventPools: Record = {}; @@ -139,6 +141,14 @@ export class UniswapV3 this.notExistingPoolSetKey = `${CACHE_PREFIX}_${network}_${dexKey}_not_existings_pool_set`.toLowerCase(); + + this.factory = new UniswapV3Factory( + dexHelper, + dexKey, + this.config.factory, + this.logger, + this.onPoolCreatedDeleteFromNonExistingSet, + ); } get supportedFees() { @@ -155,6 +165,9 @@ export class UniswapV3 } async initializePricing(blockNumber: number) { + // Init listening to new pools creation + await this.factory.initialize(blockNumber); + // This is only for testing, because cold pool fetching is goes out of // FETCH_POOL_INDENTIFIER_TIMEOUT range await Promise.all( @@ -185,6 +198,38 @@ export class UniswapV3 } } + /* + * When a non existing pool is queried, it's blacklisted for an arbitrary long period in order to prevent issuing too many rpc calls + * Once the pool is created, it gets immediately flagged + */ + onPoolCreatedDeleteFromNonExistingSet: OnPoolCreatedCallback = async ({ + token0, + token1, + fee, + }) => { + const logPrefix = '[UniswapV3.onPoolCreatedDeleteFromNonExistingSet]'; + const [_token0, _token1] = this._sortTokens(token0, token1); + const poolKey = `${token0}_${token1}_${fee}`.toLowerCase(); + + // consider doing it only from master pool for less calls to distant cache + + // delete entry locally to let local instance discover the pool + delete this.eventPools[this.getPoolIdentifier(_token0, _token1, fee)]; + + try { + this.logger.info( + `${logPrefix} delete pool from not existing set: ${poolKey}`, + ); + // delete pool record from set + await this.dexHelper.cache.zrem(this.notExistingPoolSetKey, [poolKey]); + } catch (error) { + this.logger.error( + `${logPrefix} failed to delete pool from set: ${poolKey}`, + error, + ); + } + }; + async getPool( srcAddress: Address, destAddress: Address,