From df6279523070fb7e373e1b24a994fd26703ec5d1 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Wed, 9 Aug 2023 14:14:46 +0300 Subject: [PATCH 1/7] support for custom fees: updating ABIs --- src/abis/CarbonController.json | 93 ++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/src/abis/CarbonController.json b/src/abis/CarbonController.json index 3b136f8..504ca2b 100644 --- a/src/abis/CarbonController.json +++ b/src/abis/CarbonController.json @@ -121,6 +121,11 @@ "name": "AlreadyInitialized", "type": "error" }, + { + "inputs": [], + "name": "BalanceMismatch", + "type": "error" + }, { "inputs": [], "name": "DeadlineExpired", @@ -193,27 +198,27 @@ }, { "inputs": [], - "name": "OutDated", + "name": "OrderDisabled", "type": "error" }, { "inputs": [], - "name": "Overflow", + "name": "OutDated", "type": "error" }, { "inputs": [], - "name": "PairAlreadyExists", + "name": "Overflow", "type": "error" }, { "inputs": [], - "name": "PairDoesNotExist", + "name": "PairAlreadyExists", "type": "error" }, { "inputs": [], - "name": "StrategyDoesNotExist", + "name": "PairDoesNotExist", "type": "error" }, { @@ -300,6 +305,37 @@ "name": "PairCreated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "prevFeePPM", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "newFeePPM", + "type": "uint32" + } + ], + "name": "PairTradingFeePPMUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1104,6 +1140,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + } + ], + "name": "pairTradingFeePPM", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "pairs", @@ -1225,6 +1285,29 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [ + { + "internalType": "Token", + "name": "token0", + "type": "address" + }, + { + "internalType": "Token", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint32", + "name": "newPairTradingFeePPM", + "type": "uint32" + } + ], + "name": "setPairTradingFeePPM", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { From e984484197264f1f9b5acbc474a4d867c176f734 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Sun, 13 Aug 2023 15:13:06 +0300 Subject: [PATCH 2/7] support for custom fees: implemented --- package.json | 2 +- src/chain-cache/ChainCache.ts | 50 +++++++++++++++-- src/chain-cache/ChainSync.ts | 82 +++++++++++++++++++++------ src/common/types.ts | 10 ++-- src/contracts-api/Reader.ts | 90 ++++++++++++++++++++++++------ src/strategy-management/Toolkit.ts | 11 +++- tests/ChainCache.spec.ts | 26 ++++++--- 7 files changed, 217 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 12a8d90..899be75 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bancor/carbon-sdk", "type": "module", "source": "src/index.ts", - "version": "0.0.81-DEV", + "version": "0.0.82-DEV", "description": "The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around our matching algorithm, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/chain-cache/ChainCache.ts b/src/chain-cache/ChainCache.ts index 5893b40..85cdc6a 100644 --- a/src/chain-cache/ChainCache.ts +++ b/src/chain-cache/ChainCache.ts @@ -25,7 +25,7 @@ import { import { Logger } from '../common/logger'; const logger = new Logger('ChainCache.ts'); -const schemeVersion = 5; // bump this when the serialization format changes +const schemeVersion = 6; // bump this when the serialization format changes type PairToStrategiesMap = { [key: string]: EncodedStrategy[] }; type StrategyById = { [key: string]: EncodedStrategy }; @@ -36,6 +36,7 @@ type SerializableDump = { strategiesByPair: RetypeBigNumberToString; strategiesById: RetypeBigNumberToString; ordersByDirectedPair: RetypeBigNumberToString; + tradingFeePPMByPair: { [key: string]: number }; latestBlockNumber: number; latestTradesByPair: { [key: string]: TradeData }; latestTradesByDirectedPair: { [key: string]: TradeData }; @@ -51,6 +52,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter Promise) @@ -111,6 +113,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter ), + tradingFeePPMByPair: this._tradingFeePPMByPair, latestBlockNumber: this._latestBlockNumber, latestTradesByPair: this._latestTradesByPair, latestTradesByDirectedPair: this._latestTradesByDirectedPair, @@ -184,9 +188,6 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter { + await this._checkAndHandleCacheMiss(token0, token1); + const key = toPairKey(token0, token1); + return this._tradingFeePPMByPair[key]; + } + public get blocksMetadata(): BlockMetadata[] { return this._blocksMetadata; } @@ -310,6 +320,33 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter TypedEventEmitter TypedEventEmitter { + this._tradingFeePPMByPair[toPairKey(token0, token1)] = newFee; + }); this._setLatestBlockNumber(latestBlockNumber); if (affectedPairs.size > 0) { diff --git a/src/chain-cache/ChainSync.ts b/src/chain-cache/ChainSync.ts index 0c797ed..e623250 100644 --- a/src/chain-cache/ChainSync.ts +++ b/src/chain-cache/ChainSync.ts @@ -38,26 +38,59 @@ export class ChainSync { if (this._chainCache.getLatestBlockNumber() === 0) { logger.debug('startDataSync - cache is new', arguments); // cache starts from scratch so we want to avoid getting events from the beginning of time - this._chainCache.applyBatchedUpdates(blockNumber, [], [], [], []); + this._chainCache.applyBatchedUpdates(blockNumber, [], [], [], [], []); } + // let's fetch all pairs from the chain and set them to the cache - to be used by the following syncs + await this._updatePairsFromChain(); + + // _populateFeesData() should run first, before _populatePairsData() gets to manipulate the pairs list await Promise.all([ - this._trackFees(), + this._populateFeesData(), this._populatePairsData(), this._syncEvents(), ]); } - private async _trackFees(): Promise { - logger.debug('_trackFees called'); - const tradingFeePPM = await this._fetcher.tradingFeePPM(); - this._chainCache.tradingFeePPM = tradingFeePPM; - this._fetcher.onTradingFeePPMUpdated( - (prevFeePPM: number, newFeePPM: number) => { - logger.debug('tradingFeePPM updated from', prevFeePPM, 'to', newFeePPM); - this._chainCache.tradingFeePPM = newFeePPM; - } + // reads all pairs from chain and sets to private field + private async _updatePairsFromChain() { + logger.debug('_updatePairsFromChain fetches pairs'); + this._pairs = [...(await this._fetcher.pairs())]; + logger.debug('_updatePairsFromChain fetched pairs', this._pairs); + this._lastFetch = Date.now(); + if (this._pairs.length === 0) { + logger.error( + '_updatePairsFromChain fetched no pairs - this indicates a problem' + ); + } + } + + // this is a one time operation that populates the fees data + private async _populateFeesData(): Promise { + logger.debug('populateFeesData called'); + if (this._pairs.length === 0) { + logger.error('populateFeesData called with no pairs - skipping'); + return; + } + const uncachedPairs = this._pairs.filter( + (pair) => !this._chainCache.hasCachedPair(pair[0], pair[1]) ); + + logger.debug( + 'populateFeesData found', + uncachedPairs.length, + 'uncached pairs that requires fees update' + ); + if (uncachedPairs.length === 0) return; + + const feeUpdates: [string, string, number][] = + await this._fetcher.pairsTradingFeePPM(uncachedPairs); + + logger.debug('populateFeesData fetched fee updates', feeUpdates); + + feeUpdates.forEach((feeUpdate) => { + this._chainCache.addPairFees(feeUpdate[0], feeUpdate[1], feeUpdate[2]); + }); } // `_populatePairsData` sets timeout and returns immediately. It does the following: @@ -85,10 +118,7 @@ export class ChainSync { setTimeout(processPairs, 1000); return; } - logger.debug('_populatePairsData fetches pairs'); - this._pairs = [...(await this._fetcher.pairs())]; - logger.debug('_populatePairsData fetched pairs', this._pairs); - this._lastFetch = Date.now(); + this._updatePairsFromChain(); } // let's find the first pair that's not in the cache and clear it from the list along with all the items before it const nextPairToSync = findAndRemoveLeading( @@ -165,7 +195,14 @@ export class ChainSync { if (await this._detectReorg(currentBlock)) { logger.debug('_syncEvents detected reorg - resetting'); this._chainCache.clear(); - this._chainCache.applyBatchedUpdates(currentBlock, [], [], [], []); + this._chainCache.applyBatchedUpdates( + currentBlock, + [], + [], + [], + [], + [] + ); this._resetPairsFetching(); setTimeout(processEvents, 1); return; @@ -193,6 +230,7 @@ export class ChainSync { const updatedStrategiesChunks: EncodedStrategy[][] = []; const deletedStrategiesChunks: EncodedStrategy[][] = []; const tradesChunks: TradeData[][] = []; + const feeUpdatesChunks: [string, string, number][][] = []; for (const blockChunk of blockChunks) { logger.debug('_syncEvents fetches events for chunk', blockChunk); @@ -216,11 +254,17 @@ export class ChainSync { blockChunk[0], blockChunk[1] ); + const feeUpdatesChunk: [string, string, number][] = + await this._fetcher.getLatestPairTradingFeeUpdates( + blockChunk[0], + blockChunk[1] + ); createdStrategiesChunks.push(createdStrategiesChunk); updatedStrategiesChunks.push(updatedStrategiesChunk); deletedStrategiesChunks.push(deletedStrategiesChunk); tradesChunks.push(tradesChunk); + feeUpdatesChunks.push(feeUpdatesChunk); logger.debug( '_syncEvents fetched the following events for chunks', blockChunks, @@ -229,6 +273,7 @@ export class ChainSync { updatedStrategiesChunk, deletedStrategiesChunk, tradesChunk, + feeUpdatesChunk, } ); } @@ -237,13 +282,15 @@ export class ChainSync { const updatedStrategies = updatedStrategiesChunks.flat(); const deletedStrategies = deletedStrategiesChunks.flat(); const trades = tradesChunks.flat(); + const feeUpdates = feeUpdatesChunks.flat(); logger.debug( '_syncEvents fetched events', createdStrategies, updatedStrategies, deletedStrategies, - trades + trades, + feeUpdates ); // let's check created strategies and see if we have a pair that's not cached yet, @@ -264,6 +311,7 @@ export class ChainSync { this._chainCache.applyBatchedUpdates( currentBlock, + feeUpdates, trades.filter((trade) => cachedPairs.has(toPairKey(trade.sourceToken, trade.targetToken)) ), diff --git a/src/common/types.ts b/src/common/types.ts index 3d2782d..5c4cd83 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -144,6 +144,8 @@ export type BlockMetadata = { export interface Fetcher { pairs(): Promise; strategiesByPair(token0: string, token1: string): Promise; + pairTradingFeePPM(token0: string, token1: string): Promise; + pairsTradingFeePPM(pairs: TokenPair[]): Promise<[string, string, number][]>; getLatestStrategyCreatedStrategies( fromBlock: number, toBlock: number @@ -160,10 +162,10 @@ export interface Fetcher { fromBlock: number, toBlock: number ): Promise; + getLatestPairTradingFeeUpdates( + fromBlock: number, + toBlock: number + ): Promise<[string, string, number][]>; // [token0, token1, feePPM] getBlockNumber(): Promise; - tradingFeePPM(): Promise; - onTradingFeePPMUpdated( - listener: (prevFeePPM: number, newFeePPM: number) => void - ): void; getBlock(blockNumber: number): Promise; } diff --git a/src/contracts-api/Reader.ts b/src/contracts-api/Reader.ts index 6188618..9fb2ee1 100644 --- a/src/contracts-api/Reader.ts +++ b/src/contracts-api/Reader.ts @@ -5,6 +5,7 @@ import { StrategyCreatedEventObject, StrategyUpdatedEventObject, StrategyDeletedEventObject, + PairTradingFeePPMUpdatedEventObject, } from '../abis/types/CarbonController'; import Contracts from './Contracts'; import { isETHAddress, MultiCall, multicall } from './utils'; @@ -108,18 +109,45 @@ export default class Reader implements Fetcher { return this._contracts.voucher.tokensByOwner(owner, 0, 0); } - public tradingFeePPM(): Promise { - return this._contracts.carbonController.tradingFeePPM(); + public pairTradingFeePPM(token0: string, token1: string): Promise { + return this._contracts.carbonController.pairTradingFeePPM(token0, token1); } - public onTradingFeePPMUpdated( - listener: (prevFeePPM: number, newFeePPM: number) => void + public async pairsTradingFeePPM( + pairs: TokenPair[] + ): Promise<[string, string, number][]> { + const results = await this._multicall( + pairs.map((pair) => ({ + contractAddress: this._contracts.carbonController.address, + interface: this._contracts.carbonController.interface, + methodName: 'pairTradingFeePPM', + methodParameters: [pair[0], pair[1]], + })) + ); + if (!results || results.length === 0) return []; + return results.map((res, i) => { + return [pairs[i][0], pairs[i][1], res[0]]; + }); + } + + public onPairTradingFeePPMUpdated( + listener: ( + token0: string, + token1: string, + prevFeePPM: number, + newFeePPM: number + ) => void ) { return this._contracts.carbonController.on( - 'TradingFeePPMUpdated', - function (prevFeePPM: number, newFeePPM: number) { - logger.debug('TradingFeePPMUpdated fired with', arguments); - listener(prevFeePPM, newFeePPM); + 'PairTradingFeePPMUpdated', + function ( + token0: string, + token1: string, + prevFeePPM: number, + newFeePPM: number + ) { + logger.debug('PairTradingFeePPMUpdated fired with', arguments); + listener(token0, token1, prevFeePPM, newFeePPM); } ); } @@ -178,26 +206,26 @@ export default class Reader implements Fetcher { return strategies; }; - public getLatestStrategyCreatedStrategies = async ( + public async getLatestStrategyCreatedStrategies( fromBlock: number, toBlock: number - ): Promise => { + ): Promise { return this._getFilteredStrategies('StrategyCreated', fromBlock, toBlock); - }; + } - public getLatestStrategyUpdatedStrategies = async ( + public async getLatestStrategyUpdatedStrategies( fromBlock: number, toBlock: number - ): Promise => { + ): Promise { return this._getFilteredStrategies('StrategyUpdated', fromBlock, toBlock); - }; + } - public getLatestStrategyDeletedStrategies = async ( + public async getLatestStrategyDeletedStrategies( fromBlock: number, toBlock: number - ): Promise => { + ): Promise { return this._getFilteredStrategies('StrategyDeleted', fromBlock, toBlock); - }; + } public getLatestTokensTradedTrades = async ( fromBlock: number, @@ -234,6 +262,34 @@ export default class Reader implements Fetcher { return trades; }; + public async getLatestPairTradingFeeUpdates( + fromBlock: number, + toBlock: number + ): Promise<[string, string, number][]> { + const filter = + this._contracts.carbonController.filters.PairTradingFeePPMUpdated( + null, + null, + null, + null + ); + + const logs = await this._contracts.carbonController.queryFilter( + filter, + fromBlock, + toBlock + ); + + if (logs.length === 0) return []; + + const updates: [string, string, number][] = logs.map((log) => { + const logArgs: PairTradingFeePPMUpdatedEventObject = log.args; + return [logArgs.token0, logArgs.token1, logArgs.newFeePPM]; + }); + + return updates; + } + public getBlockNumber = async (): Promise => { return this._contracts.provider.getBlockNumber(); }; diff --git a/src/strategy-management/Toolkit.ts b/src/strategy-management/Toolkit.ts index 03c18f0..99cd525 100644 --- a/src/strategy-management/Toolkit.ts +++ b/src/strategy-management/Toolkit.ts @@ -467,10 +467,15 @@ export class Toolkit { }> { logger.debug('getTradeDataFromActions called', arguments); - const feePPM = this._cache.tradingFeePPM; + const feePPM = await this._cache.getTradingFeePPMByPair( + sourceToken, + targetToken + ); - // intentional == instead of === - if (feePPM == undefined) throw new Error('tradingFeePPM is undefined'); + if (feePPM === undefined) + throw new Error( + `tradingFeePPM is undefined for this pair: ${sourceToken}-${targetToken}` + ); const decimals = this._decimals; const sourceDecimals = await decimals.fetchDecimals(sourceToken); diff --git a/tests/ChainCache.spec.ts b/tests/ChainCache.spec.ts index 6d65a69..1709b02 100644 --- a/tests/ChainCache.spec.ts +++ b/tests/ChainCache.spec.ts @@ -57,7 +57,7 @@ describe('ChainCache', () => { cache = new ChainCache(); cache.addPair('abc', 'xyz', [encodedStrategy1, encodedStrategy2]); cache.addPair('foo', 'bar', []); - cache.applyBatchedUpdates(7, [trade], [], [], []); + cache.applyBatchedUpdates(7, [], [trade], [], [], []); serialized = cache.serialize(); deserialized = ChainCache.fromSerialized(serialized); }); @@ -127,17 +127,17 @@ describe('ChainCache', () => { affectedPairs = pairs; }); cache.addPair('abc', 'xyz', [encodedStrategy1]); - cache.applyBatchedUpdates(1, [trade], [], [], []); + cache.applyBatchedUpdates(1, [], [trade], [], [], []); expect(affectedPairs).to.deep.equal([['abc', 'xyz']]); - cache.applyBatchedUpdates(2, [], [encodedStrategy2], [], []); + cache.applyBatchedUpdates(2, [], [], [encodedStrategy2], [], []); expect(affectedPairs).to.deep.equal([['abc', 'xyz']]); - cache.applyBatchedUpdates(3, [], [], [encodedStrategy1], []); + cache.applyBatchedUpdates(3, [], [], [], [encodedStrategy1], []); expect(affectedPairs).to.deep.equal([['abc', 'xyz']]); - cache.applyBatchedUpdates(4, [], [], [], [encodedStrategy1]); + cache.applyBatchedUpdates(4, [], [], [], [], [encodedStrategy1]); expect(affectedPairs).to.deep.equal([['abc', 'xyz']]); // this shouldn't fire the event - so affectedPairs should remain the same - cache.applyBatchedUpdates(5, [], [], [], []); + cache.applyBatchedUpdates(5, [], [], [], [], []); expect(affectedPairs).to.deep.equal([['abc', 'xyz']]); }); it('should contain a single copy of a strategy that was updated', async () => { @@ -147,7 +147,7 @@ describe('ChainCache', () => { id: BigNumber.from(encodedStrategy1.id.toString()), }; cache.addPair('abc', 'xyz', [encodedStrategy1]); - cache.applyBatchedUpdates(10, [], [], [encodedStrategy1_mod], []); + cache.applyBatchedUpdates(10, [], [], [], [encodedStrategy1_mod], []); const strategies = await cache.getStrategiesByPair('abc', 'xyz'); expect(strategies).to.have.length(1); }); @@ -158,10 +158,20 @@ describe('ChainCache', () => { id: BigNumber.from(encodedStrategy1.id.toString()), }; cache.addPair('abc', 'xyz', [encodedStrategy1]); - cache.applyBatchedUpdates(10, [], [], [], [encodedStrategy1_mod]); + cache.applyBatchedUpdates(10, [], [], [], [], [encodedStrategy1_mod]); const strategies = await cache.getStrategiesByPair('abc', 'xyz'); expect(strategies).to.have.length(0); }); + it('should cache the latest fees', async () => { + const cache = new ChainCache(); + cache.applyBatchedUpdates(1, [['abc', 'xyz', 10]], [], [], [], []); + expect(await cache.getTradingFeePPMByPair('xyz', 'abc')).to.equal(10); + expect(await cache.getTradingFeePPMByPair('xyz', 'def')).to.be.undefined; + cache.applyBatchedUpdates(1, [['abc', 'xyz', 11]], [], [], [], []); + expect(await cache.getTradingFeePPMByPair('xyz', 'abc')).to.equal(11); + cache.applyBatchedUpdates(1, [['abc', 'xyz', 12], ['abc', 'xyz', 13]], [], [], [], []); + expect(await cache.getTradingFeePPMByPair('xyz', 'abc')).to.equal(13); + }); }); describe('cache miss', () => { it('getStrategiesByPair call miss handler when pair is not cached', async () => { From e44f664bd24d70ac8799fd5eba68573fb7dad72b Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Tue, 15 Aug 2023 02:16:28 +0300 Subject: [PATCH 3/7] handling fees for newly created pair and changes to default fee --- package.json | 2 +- src/chain-cache/ChainSync.ts | 67 +++++++++++++++++++++++++----------- src/common/types.ts | 8 +++++ src/contracts-api/Reader.ts | 39 +++++++++++++++++++++ 4 files changed, 94 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 899be75..69e133c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bancor/carbon-sdk", "type": "module", "source": "src/index.ts", - "version": "0.0.82-DEV", + "version": "0.0.83-DEV", "description": "The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around our matching algorithm, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/chain-cache/ChainSync.ts b/src/chain-cache/ChainSync.ts index e623250..3ec7e7b 100644 --- a/src/chain-cache/ChainSync.ts +++ b/src/chain-cache/ChainSync.ts @@ -8,6 +8,7 @@ import { TokenPair, TradeData, } from '../common/types'; +import { log } from 'console'; const logger = new Logger('ChainSync.ts'); @@ -46,7 +47,7 @@ export class ChainSync { // _populateFeesData() should run first, before _populatePairsData() gets to manipulate the pairs list await Promise.all([ - this._populateFeesData(), + this._populateFeesData(this._pairs), this._populatePairsData(), this._syncEvents(), ]); @@ -65,22 +66,21 @@ export class ChainSync { } } - // this is a one time operation that populates the fees data - private async _populateFeesData(): Promise { + private async _populateFeesData( + pairs: TokenPair[], + skipCache = false + ): Promise { logger.debug('populateFeesData called'); - if (this._pairs.length === 0) { + if (pairs.length === 0) { logger.error('populateFeesData called with no pairs - skipping'); return; } - const uncachedPairs = this._pairs.filter( - (pair) => !this._chainCache.hasCachedPair(pair[0], pair[1]) - ); - - logger.debug( - 'populateFeesData found', - uncachedPairs.length, - 'uncached pairs that requires fees update' - ); + const uncachedPairs = skipCache + ? pairs + : pairs.filter( + (pair) => !this._chainCache.hasCachedPair(pair[0], pair[1]) + ); + if (uncachedPairs.length === 0) return; const feeUpdates: [string, string, number][] = @@ -231,6 +231,7 @@ export class ChainSync { const deletedStrategiesChunks: EncodedStrategy[][] = []; const tradesChunks: TradeData[][] = []; const feeUpdatesChunks: [string, string, number][][] = []; + const defaultFeeUpdatesChunks: number[][] = []; for (const blockChunk of blockChunks) { logger.debug('_syncEvents fetches events for chunk', blockChunk); @@ -259,12 +260,18 @@ export class ChainSync { blockChunk[0], blockChunk[1] ); + const defaultFeeUpdatesChunk: number[] = + await this._fetcher.getLatestTradingFeeUpdates( + blockChunk[0], + blockChunk[1] + ); createdStrategiesChunks.push(createdStrategiesChunk); updatedStrategiesChunks.push(updatedStrategiesChunk); deletedStrategiesChunks.push(deletedStrategiesChunk); tradesChunks.push(tradesChunk); feeUpdatesChunks.push(feeUpdatesChunk); + defaultFeeUpdatesChunks.push(defaultFeeUpdatesChunk); logger.debug( '_syncEvents fetched the following events for chunks', blockChunks, @@ -274,6 +281,7 @@ export class ChainSync { deletedStrategiesChunk, tradesChunk, feeUpdatesChunk, + defaultFeeUpdatesChunk, } ); } @@ -283,6 +291,8 @@ export class ChainSync { const deletedStrategies = deletedStrategiesChunks.flat(); const trades = tradesChunks.flat(); const feeUpdates = feeUpdatesChunks.flat(); + const defaultFeeWasUpdated = + defaultFeeUpdatesChunks.flat().length > 0; logger.debug( '_syncEvents fetched events', @@ -290,22 +300,18 @@ export class ChainSync { updatedStrategies, deletedStrategies, trades, - feeUpdates + feeUpdates, + defaultFeeWasUpdated ); // let's check created strategies and see if we have a pair that's not cached yet, // which means we need to set slow poll mode to false so that it will be fetched quickly + let newlyCreatedPairs: TokenPair[] = []; for (const strategy of createdStrategies) { if ( !this._chainCache.hasCachedPair(strategy.token0, strategy.token1) ) { - logger.debug( - '_syncEvents sets slow poll mode to false because of new pair', - strategy.token0, - strategy.token1 - ); - this._slowPollPairs = false; - break; + newlyCreatedPairs.push([strategy.token0, strategy.token1]); } } @@ -325,6 +331,25 @@ export class ChainSync { cachedPairs.has(toPairKey(strategy.token0, strategy.token1)) ) ); + + // lastly - handle side effects such as new pair detected or default fee update + if (defaultFeeWasUpdated) { + logger.debug( + '_syncEvents noticed at least one default fee update - refetching pair fees for all pairs' + ); + await this._populateFeesData( + [...(await this._fetcher.pairs())], + true + ); + } + if (newlyCreatedPairs.length > 0) { + logger.debug( + '_syncEvents noticed at least one new pair created - setting slow poll mode to false' + ); + this._slowPollPairs = false; + logger.debug('_syncEvents fetching fees for the new pairs'); + await this._populateFeesData(newlyCreatedPairs, true); + } } } catch (err) { logger.error('Error syncing events:', err); diff --git a/src/common/types.ts b/src/common/types.ts index 5c4cd83..baf4fbf 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -146,6 +146,10 @@ export interface Fetcher { strategiesByPair(token0: string, token1: string): Promise; pairTradingFeePPM(token0: string, token1: string): Promise; pairsTradingFeePPM(pairs: TokenPair[]): Promise<[string, string, number][]>; + tradingFeePPM(): Promise; + onTradingFeePPMUpdated( + listener: (prevFeePPM: number, newFeePPM: number) => void + ): void; getLatestStrategyCreatedStrategies( fromBlock: number, toBlock: number @@ -162,6 +166,10 @@ export interface Fetcher { fromBlock: number, toBlock: number ): Promise; + getLatestTradingFeeUpdates( + fromBlock: number, + toBlock: number + ): Promise; getLatestPairTradingFeeUpdates( fromBlock: number, toBlock: number diff --git a/src/contracts-api/Reader.ts b/src/contracts-api/Reader.ts index 9fb2ee1..f8a52bf 100644 --- a/src/contracts-api/Reader.ts +++ b/src/contracts-api/Reader.ts @@ -109,6 +109,22 @@ export default class Reader implements Fetcher { return this._contracts.voucher.tokensByOwner(owner, 0, 0); } + public tradingFeePPM(): Promise { + return this._contracts.carbonController.tradingFeePPM(); + } + + public onTradingFeePPMUpdated( + listener: (prevFeePPM: number, newFeePPM: number) => void + ) { + return this._contracts.carbonController.on( + 'TradingFeePPMUpdated', + function (prevFeePPM: number, newFeePPM: number) { + logger.debug('TradingFeePPMUpdated fired with', arguments); + listener(prevFeePPM, newFeePPM); + } + ); + } + public pairTradingFeePPM(token0: string, token1: string): Promise { return this._contracts.carbonController.pairTradingFeePPM(token0, token1); } @@ -262,6 +278,29 @@ export default class Reader implements Fetcher { return trades; }; + public async getLatestTradingFeeUpdates( + fromBlock: number, + toBlock: number + ): Promise { + const filter = + this._contracts.carbonController.filters.TradingFeePPMUpdated(null, null); + + const logs = await this._contracts.carbonController.queryFilter( + filter, + fromBlock, + toBlock + ); + + if (logs.length === 0) return []; + + const updates: number[] = logs.map((log) => { + const logArgs = log.args; + return logArgs.newFeePPM; + }); + + return updates; + } + public async getLatestPairTradingFeeUpdates( fromBlock: number, toBlock: number From f1c402b9ec145b4b8752ce2263a69f7e84b936d0 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Tue, 15 Aug 2023 02:17:28 +0300 Subject: [PATCH 4/7] small lint --- src/chain-cache/ChainSync.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/chain-cache/ChainSync.ts b/src/chain-cache/ChainSync.ts index 3ec7e7b..d998ce1 100644 --- a/src/chain-cache/ChainSync.ts +++ b/src/chain-cache/ChainSync.ts @@ -8,7 +8,6 @@ import { TokenPair, TradeData, } from '../common/types'; -import { log } from 'console'; const logger = new Logger('ChainSync.ts'); @@ -306,7 +305,7 @@ export class ChainSync { // let's check created strategies and see if we have a pair that's not cached yet, // which means we need to set slow poll mode to false so that it will be fetched quickly - let newlyCreatedPairs: TokenPair[] = []; + const newlyCreatedPairs: TokenPair[] = []; for (const strategy of createdStrategies) { if ( !this._chainCache.hasCachedPair(strategy.token0, strategy.token1) From 8386e73819a474c7a235bdec3f3238558191c6be Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Tue, 15 Aug 2023 02:23:41 +0300 Subject: [PATCH 5/7] small fix --- src/chain-cache/ChainSync.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/chain-cache/ChainSync.ts b/src/chain-cache/ChainSync.ts index d998ce1..898ea44 100644 --- a/src/chain-cache/ChainSync.ts +++ b/src/chain-cache/ChainSync.ts @@ -101,9 +101,6 @@ export class ChainSync { // 6. if there are no more pairs, it sets a timeout to call itself again private async _populatePairsData(): Promise { logger.debug('_populatePairsData called'); - this._pairs = []; - // keep the time stamp of last fetch - this._lastFetch = Date.now(); // this indicates we want to poll for pairs only once a minute. // Set this to false when we have an indication that new pair was created - which we want to fetch now this._slowPollPairs = false; From 127d0f8dde0e9460b8e1b753b1dd7a26cc567173 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Tue, 15 Aug 2023 03:46:45 +0300 Subject: [PATCH 6/7] fixed handling of created strategy --- src/chain-cache/ChainSync.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chain-cache/ChainSync.ts b/src/chain-cache/ChainSync.ts index 898ea44..300e445 100644 --- a/src/chain-cache/ChainSync.ts +++ b/src/chain-cache/ChainSync.ts @@ -114,7 +114,7 @@ export class ChainSync { setTimeout(processPairs, 1000); return; } - this._updatePairsFromChain(); + await this._updatePairsFromChain(); } // let's find the first pair that's not in the cache and clear it from the list along with all the items before it const nextPairToSync = findAndRemoveLeading( @@ -206,7 +206,7 @@ export class ChainSync { const cachedPairs = new Set( this._chainCache - .getCachedPairs() + .getCachedPairs(false) .map((pair) => toPairKey(pair[0], pair[1])) ); From ae4b558ec4f50850fc4b2b41ec22eaca2c2959f7 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Thu, 17 Aug 2023 01:41:36 +0300 Subject: [PATCH 7/7] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 69e133c..017849c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bancor/carbon-sdk", "type": "module", "source": "src/index.ts", - "version": "0.0.83-DEV", + "version": "0.0.84-DEV", "description": "The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around our matching algorithm, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades", "main": "dist/index.js", "module": "dist/index.js",