@bancor/carbon-sdk
Version:
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 fulfi
2 lines (1 loc) • 15.1 kB
JavaScript
"use strict";var e=require("events"),t=require("./serializers-DfRMJ_-P.cjs"),a=require("./logger-d0ZZl9KP.cjs");const r=(e,t)=>e.localeCompare(t),s="->-<-",i=e=>{if(2!==e.length)throw new Error(`Invalid number of tokens: expected 2, got ${e.length}`);if(e[0]===e[1])throw new Error(`Cannot create key for identical tokens: ${e[0]}`);return e.join(s)},n=(e,t)=>i([e,t].sort(r)),c=e=>{const t=e.split(s);return[t[0],t[1]]},o=(e,t)=>i([e,t]);const h=new a.Logger("ChainCache.ts");class d extends e{_strategiesByPair={};_strategiesById={};_ordersByDirectedPair={};_latestBlockNumber=0;_latestTradesByPair={};_latestTradesByDirectedPair={};_blocksMetadata=[];_tradingFeePPMByPair={};_isCacheInitialized=!1;_handleCacheMiss;static fromSerialized(e){try{const t=new d;return t._deserialize(e),t}catch(e){h.error("Failed to deserialize cache, returning clear cache",e)}return new d}_deserialize(e){const a=JSON.parse(e),{schemeVersion:r}=a;6===r?"number"==typeof a.latestBlockNumber?(this._strategiesByPair=Object.entries(a.strategiesByPair).reduce(((e,[a,r])=>(e[a]=r.map(t.encodedStrategyStrToBN),e)),{}),this._strategiesById=Object.entries(a.strategiesById).reduce(((e,[a,r])=>(e[a]=t.encodedStrategyStrToBN(r),e)),{}),this._ordersByDirectedPair=Object.entries(a.ordersByDirectedPair).reduce(((e,[a,r])=>(e[a]=Object.entries(r).reduce(((e,[a,r])=>(e[a]=t.encodedOrderStrToBN(r),e)),{}),e)),{}),this._tradingFeePPMByPair=a.tradingFeePPMByPair,this._latestBlockNumber=a.latestBlockNumber,this._latestTradesByPair=a.latestTradesByPair,this._latestTradesByDirectedPair=a.latestTradesByDirectedPair,a.blocksMetadata&&(this._blocksMetadata=a.blocksMetadata.filter((e=>!!e&&!!e.number&&!!e.hash)))):h.error("Cached latest block number is not a number, ignoring cache"):h.log("Cache version mismatch, ignoring cache. Expected",6,"got",r,"This may be due to a breaking change in the cache format since it was last persisted.")}serialize(){const e={schemeVersion:6,strategiesByPair:Object.entries(this._strategiesByPair).reduce(((e,[a,r])=>(e[a]=r.map(t.encodedStrategyBNToStr),e)),{}),strategiesById:Object.entries(this._strategiesById).reduce(((e,[a,r])=>(e[a]=t.encodedStrategyBNToStr(r),e)),{}),ordersByDirectedPair:Object.entries(this._ordersByDirectedPair).reduce(((e,[a,r])=>(e[a]=Object.entries(r).reduce(((e,[a,r])=>(e[a]=t.encodedOrderBNToStr(r),e)),{}),e)),{}),tradingFeePPMByPair:this._tradingFeePPMByPair,latestBlockNumber:this._latestBlockNumber,latestTradesByPair:this._latestTradesByPair,latestTradesByDirectedPair:this._latestTradesByDirectedPair,blocksMetadata:this._blocksMetadata};return JSON.stringify(e)}setCacheMissHandler(e){this._handleCacheMiss=e}async _checkAndHandleCacheMiss(e,t){this._isCacheInitialized||!this._handleCacheMiss||this.hasCachedPair(e,t)||(h.debug("Cache miss for pair",e,t),await this._handleCacheMiss(e,t),h.debug("Cache miss for pair",e,t,"resolved"))}clear(e=!1){const t=Object.keys(this._strategiesByPair).map(c);this._strategiesByPair={},this._strategiesById={},this._ordersByDirectedPair={},this._latestBlockNumber=0,this._latestTradesByPair={},this._latestTradesByDirectedPair={},this._blocksMetadata=[],e||this.emit("onPairDataChanged",t)}async getStrategiesByPair(e,t){await this._checkAndHandleCacheMiss(e,t);const a=n(e,t);return this._strategiesByPair[a]}async getStrategiesByPairs(e){const t=[];for(const a of e){const e=await this.getStrategiesByPair(a[0],a[1]);e&&t.push({pair:a,strategies:e})}return t}getStrategyById(e){return this._strategiesById[e.toString()]}getCachedPairs(e=!0){return e?Object.entries(this._strategiesByPair).filter((([e,t])=>t.length>0)).map((([e,t])=>c(e))):Object.keys(this._strategiesByPair).map(c)}async getOrdersByPair(e,t,a=!1){await this._checkAndHandleCacheMiss(e,t);const r=o(e,t),s=this._ordersByDirectedPair[r]||{};return a?s:Object.fromEntries(Object.entries(s).filter((([e,t])=>function(e){return e.y.gt(0)&&(e.A.gt(0)||e.B.gt(0))}(t))))}hasCachedPair(e,t){const a=n(e,t);return!!this._strategiesByPair[a]}async getLatestTradeByPair(e,t){await this._checkAndHandleCacheMiss(e,t);const a=n(e,t);return this._latestTradesByPair[a]}async getLatestTradeByDirectedPair(e,t){await this._checkAndHandleCacheMiss(e,t);const a=o(e,t);return this._latestTradesByDirectedPair[a]}getLatestTrades(){return Object.values(this._latestTradesByPair)}getLatestBlockNumber(){return this._latestBlockNumber}async getTradingFeePPMByPair(e,t){await this._checkAndHandleCacheMiss(e,t);const a=n(e,t);return this._tradingFeePPMByPair[a]}get blocksMetadata(){return this._blocksMetadata}set blocksMetadata(e){this._blocksMetadata=e}_addPair(e,t,a){h.debug("Adding pair with",a.length," strategies to cache",e,t);const r=n(e,t);if(this._strategiesByPair[r])throw new Error(`Pair ${r} already cached`);this._strategiesByPair[r]=a,a.forEach((e=>{this._strategiesById[e.id.toString()]=e,this._addStrategyOrders(e)}))}addPair(e,t,a){this._addPair(e,t,a),h.debug("Emitting onPairAddedToCache",e,t),this.emit("onPairAddedToCache",c(n(e,t)))}bulkAddPairs(e){h.debug("Bulk adding pairs",e);for(const t of e)this._addPair(t.pair[0],t.pair[1],t.strategies);e.length>0&&!this._isCacheInitialized&&(this._isCacheInitialized=!0,h.debug("Emitting onCacheInitialized"),this.emit("onCacheInitialized"))}addPairFees(e,t,a){h.debug("Adding trading fee to pair",e,t,"fee",a);const r=n(e,t);this._tradingFeePPMByPair[r]=a}applyEvents(e,t){const a=new Set;this._setLatestBlockNumber(t);for(const t of e)switch(t.type){case"StrategyCreated":{const e=t.data;this._addStrategy(e),a.add(n(e.token0,e.token1));break}case"StrategyUpdated":{const e=t.data;this._updateStrategy(e),a.add(n(e.token0,e.token1));break}case"StrategyDeleted":{const e=t.data;this._deleteStrategy(e),a.add(n(e.token0,e.token1));break}case"TokensTraded":{const e=t.data;this._setLatestTrade(e),a.add(n(e.sourceToken,e.targetToken));break}case"PairTradingFeePPMUpdated":{const e=t.data;this.addPairFees(e[0],e[1],e[2]);break}}a.size>0&&(h.debug("Emitting onPairDataChanged",a),this.emit("onPairDataChanged",Array.from(a).map(c)))}_setLatestBlockNumber(e){this._latestBlockNumber=e}_setLatestTrade(e){if(!this.hasCachedPair(e.sourceToken,e.targetToken))return void h.error(`Pair ${n(e.sourceToken,e.targetToken)} is not cached, cannot set latest trade`);const t=n(e.sourceToken,e.targetToken);this._latestTradesByPair[t]=e;const a=o(e.sourceToken,e.targetToken);this._latestTradesByDirectedPair[a]=e}_addStrategyOrders(e){for(const t of[[e.token0,e.token1],[e.token1,e.token0]]){const a=o(t[0],t[1]),r=t[0]===e.token0?e.order1:e.order0,s=this._ordersByDirectedPair[a];s?s[e.id.toString()]=r:this._ordersByDirectedPair[a]={[e.id.toString()]:r}}}_removeStrategyOrders(e){for(const t of[[e.token0,e.token1],[e.token1,e.token0]]){const a=o(t[0],t[1]),r=this._ordersByDirectedPair[a];r&&(delete r[e.id.toString()],0===Object.keys(r).length&&delete this._ordersByDirectedPair.key)}}_addStrategy(e){if(!this.hasCachedPair(e.token0,e.token1))return void h.error(`Pair ${n(e.token0,e.token1)} is not cached, cannot add strategy`);const t=n(e.token0,e.token1);if(this._strategiesById[e.id.toString()])return void h.debug(`Strategy ${e.id} already cached, under the pair ${t} - skipping`);const a=this._strategiesByPair[t]||[];a.push(e),this._strategiesByPair[t]=a,this._strategiesById[e.id.toString()]=e,this._addStrategyOrders(e)}_updateStrategy(e){if(!this.hasCachedPair(e.token0,e.token1))return void h.error(`Pair ${n(e.token0,e.token1)} is not cached, cannot update strategy`);const t=n(e.token0,e.token1),a=(this._strategiesByPair[t]||[]).filter((t=>!t.id.eq(e.id)));a.push(e),this._strategiesByPair[t]=a,this._strategiesById[e.id.toString()]=e,this._removeStrategyOrders(e),this._addStrategyOrders(e)}_deleteStrategy(e){if(!this.hasCachedPair(e.token0,e.token1))return void h.error(`Pair ${n(e.token0,e.token1)} is not cached, cannot delete strategy`);const t=n(e.token0,e.token1);delete this._strategiesById[e.id.toString()];const a=(this._strategiesByPair[t]||[]).filter((t=>!t.id.eq(e.id)));this._strategiesByPair[t]=a,this._removeStrategyOrders(e)}}const l=new a.Logger("ChainSync.ts");class g{_fetcher;_chainCache;_syncCalled=!1;_slowPollPairs=!1;_uncachedPairs=[];_lastFetch=Date.now();_numOfPairsToBatch;_msToWaitBetweenSyncs;_chunkSize;_activeTimers=new Set;_isStopped=!1;constructor(e,t,a=100,r=1e3,s=1e3){this._fetcher=e,this._chainCache=t,this._numOfPairsToBatch=a,this._msToWaitBetweenSyncs=r,this._chunkSize=s}stop(){l.debug("Stopping all ChainSync timers"),this._isStopped=!0,this._activeTimers.forEach((e=>clearTimeout(e))),this._activeTimers.clear()}_setTimeout(e,t){if(this._isStopped)return l.debug("Ignoring timer creation after stop() was called"),0;const a=Number(setTimeout((()=>{this._activeTimers.delete(a),e()}),t));return this._activeTimers.add(a),a}async startDataSync(){if(l.debug("startDataSync called"),this._syncCalled)throw new Error("ChainSync.startDataSync() can only be called once");this._syncCalled=!0;if(0===this._chainCache.getLatestBlockNumber()){l.debug("startDataSync - cache is new",arguments);const e=await this._fetcher.getBlockNumber();if("number"!=typeof e)throw l.error("Fatal! startDataSync - getBlockNumber returned value is not a number. This indicates a serious bug in the provider. At this point, the only thing we can do is to crash the program, as the cache cannot be set to a valid state."),new Error("Fatal! startDataSync - getBlockNumber returned value is not a number.");this._chainCache.applyEvents([],e)}await this._updateUncachedPairsFromChain(),await Promise.all([this._populateFeesData(this._uncachedPairs),this._populatePairsData(),this._syncEvents()])}async _updateUncachedPairsFromChain(){l.debug("_updateUncachedPairsFromChain fetches pairs");const e=await this._fetcher.pairs();l.debug("_updateUncachedPairsFromChain fetched pairs",e),this._lastFetch=Date.now(),0===e.length&&l.error("_updateUncachedPairsFromChain fetched no pairs - this indicates a problem"),this._uncachedPairs=e.filter((e=>!this._chainCache.hasCachedPair(e[0],e[1])))}async _populateFeesData(e){if(l.debug("populateFeesData called"),0===e.length)return void l.log("populateFeesData called with no pairs - skipping");const t=await this._fetcher.pairsTradingFeePPM(e);l.debug("populateFeesData fetched fee updates",t),t.forEach((e=>{this._chainCache.addPairFees(e[0],e[1],e[2])}))}async _populatePairsData(){l.debug("_populatePairsData called"),this._slowPollPairs=!1;const e=async()=>{try{if(0===this._uncachedPairs.length){if(this._slowPollPairs&&Date.now()-this._lastFetch<6e4)return void this._setTimeout(e,1e3);await this._updateUncachedPairsFromChain()}return this._uncachedPairs.length>0&&(l.debug("_populatePairsData will now sync data for",this._uncachedPairs),await this._syncPairDataBatch()),l.debug("_populatePairsData handled all pairs and goes to slow poll mode"),this._slowPollPairs=!0,void this._setTimeout(e,1e3)}catch(t){l.error("Error while syncing pairs data",t),this._setTimeout(e,6e4)}};await e()}async _syncPairDataBatch(){const e=[];for(let t=0;t<this._uncachedPairs.length;t+=this._numOfPairsToBatch)e.push(this._uncachedPairs.slice(t,t+this._numOfPairsToBatch));l.debug("_syncPairDataBatch batches",e);try{const t=await Promise.all(e.map((e=>this._fetcher.strategiesByPairs(e))));l.debug("_syncPairDataBatch strategiesBatches",t),this._chainCache.bulkAddPairs(t.flat()),this._uncachedPairs=[]}catch(e){throw l.error("Failed to fetch strategies for pairs batch:",e),e}}async syncPairData(e,t){if(!this._syncCalled)throw new Error("ChainSync.startDataSync() must be called before syncPairData()");try{const a=await this._fetcher.strategiesByPair(e,t);if(this._chainCache.hasCachedPair(e,t))return;this._chainCache.addPair(e,t,a)}catch(a){l.error("Failed to fetch strategies for pair:",e,t,a)}}async _syncEvents(){l.debug("_syncEvents called");const e=async()=>{l.debug("_syncEvents processEvents - new cycle started");try{const t=await this._fetcher.getBlockNumber();if("number"!=typeof t)throw l.error("_syncEvents - getBlockNumber returned value is not a number. This indicates a serious bug in the provider. Throwing an error in hope that the next iteration will get a valid number."),new Error("_syncEvents - getBlockNumber returned value is not a number.");const a=this._chainCache.getLatestBlockNumber();if(l.debug("_syncEvents processEvents - latestBlock (start point for new cycle)",a,"currentBlock",t),t>a){if(await this._detectReorg(t))return l.debug("_syncEvents detected reorg - resetting"),this._chainCache.clear(),this._chainCache.applyEvents([],t),this._resetPairsFetching(),void this._setTimeout(e,1);l.debug("_syncEvents fetches events",a+1,t);const r=await this._fetcher.getEvents(a+1,t,this._chunkSize);l.debug("_syncEvents fetched events",r);const s=[];for(const e of r)if("StrategyCreated"===e.type){const t=e.data;this._chainCache.hasCachedPair(t.token0,t.token1)||(l.debug("_syncEvents noticed new pair created",t.token0,t.token1),s.push([t.token0,t.token1]))}this._chainCache.applyEvents(r,t),r.some((e=>"TradingFeePPMUpdated"===e.type))&&(l.debug("_syncEvents noticed at least one default fee update - refetching pair fees for all pairs"),await this._populateFeesData([...await this._fetcher.pairs()])),s.length>0&&(l.debug("_syncEvents noticed at least one new pair created - setting slow poll mode to false"),this._slowPollPairs=!1,l.debug("_syncEvents fetching fees for the new pairs"),await this._populateFeesData(s))}}catch(e){l.error("Error syncing events:",e)}this._setTimeout(e,this._msToWaitBetweenSyncs)};this._setTimeout(e,1)}_resetPairsFetching(){this._uncachedPairs=[],this._slowPollPairs=!1}async _detectReorg(e){l.debug("_detectReorg called");const t=this._chainCache.blocksMetadata,a={};for(const r of t){const{number:t,hash:s}=r;if(t>e)return l.log("reorg detected for block number",t,"larger than current block",e,"with hash",s),!0;try{const e=await this._fetcher.getBlock(t);if(!e||!e.hash||!e.number)return l.error("Failed to fetch block data for block number",t,"- treating as potential reorg"),!0;const i=e.hash;if(s!==i)return l.log("reorg detected for block number",t,"old hash",s,"new hash",i),!0;a[t]={number:r.number,hash:r.hash}}catch(e){return l.error("Error fetching block data for block number",t,":",e),!0}}l.debug("_detectReorg no reorg detected, updating blocks metadata");const r=[];for(let t=0;t<3;t++){const s=e-t;if(a[s])r.push(a[s]);else try{const e=await this._fetcher.getBlock(s);if(!e||!e.number||!e.hash){l.error("Failed to fetch new block data for block number",s,"- skipping this block");continue}r.push({number:e.number,hash:e.hash})}catch(e){l.error("Error fetching new block data for block number",s,":",e,"- skipping this block");continue}}return this._chainCache.blocksMetadata=r,l.debug("_detectReorg updated blocks metadata"),!1}}const _=(e,t,a,r,s)=>{let i;t&&(i=d.fromSerialized(t)),i||(i=new d);const n=new g(e,i,a,r,s);return i.setCacheMissHandler(n.syncPairData.bind(n)),{cache:i,startDataSync:n.startDataSync.bind(n)}};var u=Object.freeze({__proto__:null,ChainCache:d,ChainSync:g,initSyncedCache:_});exports.ChainCache=d,exports.ChainSync=g,exports.index=u,exports.initSyncedCache=_;