UNPKG

@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) 13.6 kB
"use strict";var e=require("events"),t=require("./utils-Dk1Vz9Yd.cjs"),a=require("./serializers-gTido5tE.cjs"),i=require("./logger-BbRNJAwH.cjs");const r=new i.Logger("ChainCache.ts");class s extends e{_strategiesByPair={};_strategiesById={};_ordersByDirectedPair={};_latestBlockNumber=0;_blocksMetadata=[];_tradingFeePPMByPair={};_isCacheInitialized=!1;_handleCacheMiss;static fromSerialized(e){try{const t=new s;return t._deserialize(e),t}catch(e){r.error("Failed to deserialize cache, returning clear cache",e)}return new s}_deserialize(e){const i=JSON.parse(e),{schemeVersion:s}=i;if(7===s)if("number"==typeof i.latestBlockNumber){for(const[e,r]of Object.entries(i.strategiesByPair)){const[i,s]=t.fromPairKey(e);this._addPair(i,s,r.map(a.encodedStrategyStrToBN))}this._tradingFeePPMByPair=i.tradingFeePPMByPair,this._latestBlockNumber=i.latestBlockNumber,this._isCacheInitialized=!0,r.debug("Cache initialized from serialized data")}else r.error("Cached latest block number is not a number, ignoring cache");else r.log("Cache version mismatch, ignoring cache. Expected",7,"got",s,"This may be due to a breaking change in the cache format since it was last persisted.")}serialize(){const e={schemeVersion:7,strategiesByPair:Object.entries(this._strategiesByPair).reduce(((e,[t,i])=>(e[t]=i.map(a.encodedStrategyBigIntToStr),e)),{}),tradingFeePPMByPair:this._tradingFeePPMByPair,latestBlockNumber:this._latestBlockNumber};return JSON.stringify(e)}setCacheMissHandler(e){this._handleCacheMiss=e}async _checkAndHandleCacheMiss(e,t){this._isCacheInitialized||!this._handleCacheMiss||this.hasCachedPair(e,t)||(r.debug("Cache miss for pair",e,t),await this._handleCacheMiss(e,t),r.debug("Cache miss for pair",e,t,"resolved"))}isCacheInitialized(){return this._isCacheInitialized}clear(){this._strategiesByPair={},this._strategiesById={},this._ordersByDirectedPair={},this._latestBlockNumber=0,this._blocksMetadata=[],this._blocksMetadata=[],this._tradingFeePPMByPair={},this._isCacheInitialized=!1,this.emit("onCacheCleared")}async getStrategiesByPair(e,a){await this._checkAndHandleCacheMiss(e,a);const i=t.toPairKey(e,a);return this._strategiesByPair[i]}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,a])=>t.fromPairKey(e))):Object.keys(this._strategiesByPair).map(t.fromPairKey)}async getOrdersByPair(e,a,i=!1){await this._checkAndHandleCacheMiss(e,a);const r=t.toDirectionKey(e,a),s=this._ordersByDirectedPair[r]||{};return i?s:Object.fromEntries(Object.entries(s).filter((([e,a])=>t.isOrderTradable(a))))}hasCachedPair(e,a){const i=t.toPairKey(e,a);return!!this._strategiesByPair[i]}getLatestBlockNumber(){return this._latestBlockNumber}async getTradingFeePPMByPair(e,a){await this._checkAndHandleCacheMiss(e,a);const i=t.toPairKey(e,a);return this._tradingFeePPMByPair[i]}get blocksMetadata(){return this._blocksMetadata}set blocksMetadata(e){this._blocksMetadata=e}_addPair(e,a,i){r.debug("Adding pair with",i.length," strategies to cache",e,a);const s=t.toPairKey(e,a);if(this._strategiesByPair[s])throw new Error(`Pair ${s} already cached`);this._strategiesByPair[s]=i,i.forEach((e=>{this._strategiesById[e.id.toString()]=e,this._addStrategyOrders(e)}))}addPair(e,a,i){this._addPair(e,a,i),r.debug("Emitting onPairAddedToCache",e,a),this.emit("onPairAddedToCache",t.fromPairKey(t.toPairKey(e,a)))}bulkAddPairs(e){r.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,r.debug("Emitting onCacheInitialized"),this.emit("onCacheInitialized"))}addPairFees(e,a,i){r.debug("Adding trading fee to pair",e,a,"fee",i);const s=t.toPairKey(e,a);this._tradingFeePPMByPair[s]=i}applyEvents(e,a){const i=new Set;this._setLatestBlockNumber(a);for(const a of e)switch(a.type){case"StrategyCreated":{const e=a.data;this._addStrategy(e),i.add(t.toPairKey(e.token0,e.token1));break}case"StrategyUpdated":{const e=a.data;this._updateStrategy(e),i.add(t.toPairKey(e.token0,e.token1));break}case"StrategyDeleted":{const e=a.data;this._deleteStrategy(e),i.add(t.toPairKey(e.token0,e.token1));break}case"PairTradingFeePPMUpdated":{const e=a.data;this.addPairFees(e[0],e[1],e[2]);break}}i.size>0&&(r.debug("Emitting onPairDataChanged",i),this.emit("onPairDataChanged",Array.from(i).map(t.fromPairKey)))}_setLatestBlockNumber(e){this._latestBlockNumber=e}_addStrategyOrders(e){for(const a of[[e.token0,e.token1],[e.token1,e.token0]]){const i=t.toDirectionKey(a[0],a[1]),r=a[0]===e.token0?e.order1:e.order0,s=this._ordersByDirectedPair[i];s?s[e.id.toString()]=r:this._ordersByDirectedPair[i]={[e.id.toString()]:r}}}_removeStrategyOrders(e){for(const a of[[e.token0,e.token1],[e.token1,e.token0]]){const i=t.toDirectionKey(a[0],a[1]),r=this._ordersByDirectedPair[i];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 r.error(`Pair ${t.toPairKey(e.token0,e.token1)} is not cached, cannot add strategy`);const a=t.toPairKey(e.token0,e.token1);if(this._strategiesById[e.id.toString()])return void r.debug(`Strategy ${e.id} already cached, under the pair ${a} - skipping`);const i=this._strategiesByPair[a]||[];i.push(e),this._strategiesByPair[a]=i,this._strategiesById[e.id.toString()]=e,this._addStrategyOrders(e)}_updateStrategy(e){if(!this.hasCachedPair(e.token0,e.token1))return void r.error(`Pair ${t.toPairKey(e.token0,e.token1)} is not cached, cannot update strategy`);const a=t.toPairKey(e.token0,e.token1),i=(this._strategiesByPair[a]||[]).filter((t=>t.id!==e.id));i.push(e),this._strategiesByPair[a]=i,this._strategiesById[e.id.toString()]=e,this._removeStrategyOrders(e),this._addStrategyOrders(e)}_deleteStrategy(e){if(!this.hasCachedPair(e.token0,e.token1))return void r.error(`Pair ${t.toPairKey(e.token0,e.token1)} is not cached, cannot delete strategy`);const a=t.toPairKey(e.token0,e.token1);delete this._strategiesById[e.id.toString()];const i=(this._strategiesByPair[a]||[]).filter((t=>t.id!==e.id));this._strategiesByPair[a]=i,this._removeStrategyOrders(e)}}const n=new i.Logger("ChainSync.ts");class c{_fetcher;_chainCache;_syncCalled=!1;_slowPollPairs=!1;_uncachedPairs=[];_lastFetch=Date.now();_numOfPairsToBatch;_msToWaitBetweenSyncs;_chunkSize;_activeTimers=new Set;_isStopped=!1;_reinitializeSelf(){this._syncCalled=!1,this._slowPollPairs=!1,this._uncachedPairs=[],this._lastFetch=Date.now(),this._activeTimers=new Set,this._isStopped=!1}_resetSelf(){const e=this._syncCalled;this.stop(),this._reinitializeSelf(),this._chainCache.clear(),e&&this.startDataSync()}constructor(e,t,a=100,i=1e3,r=1e3){this._reinitializeSelf(),this._fetcher=e,this._chainCache=t,this._numOfPairsToBatch=a,this._msToWaitBetweenSyncs=i,this._chunkSize=r}stop(){n.debug("Stopping all ChainSync timers"),this._isStopped=!0,this._activeTimers.forEach((e=>clearTimeout(e))),this._activeTimers.clear()}_setTimeout(e,t){if(this._isStopped)return n.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(n.debug("startDataSync called"),this._syncCalled)throw new Error("ChainSync.startDataSync() can only be called once");this._syncCalled=!0;if(0===this._chainCache.getLatestBlockNumber()){n.debug("startDataSync - cache is new",arguments);const e=await this._fetcher.getBlockNumber();if("number"!=typeof e)throw n.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(){n.debug("_updateUncachedPairsFromChain fetches pairs");const e=await this._fetcher.pairs();n.debug("_updateUncachedPairsFromChain fetched pairs",e),this._lastFetch=Date.now(),0===e.length&&n.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(n.debug("populateFeesData called"),0===e.length)return void n.log("populateFeesData called with no pairs - skipping");const t=await this._fetcher.pairsTradingFeePPM(e);n.debug("populateFeesData fetched fee updates",t),t.forEach((e=>{this._chainCache.addPairFees(e[0],e[1],e[2])}))}async _populatePairsData(){n.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&&(n.debug("_populatePairsData will now sync data for",this._uncachedPairs),await this._syncPairDataBatch()),n.debug("_populatePairsData handled all pairs and goes to slow poll mode"),this._slowPollPairs=!0,void this._setTimeout(e,1e3)}catch(t){n.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));n.debug("_syncPairDataBatch batches",e);try{const t=await Promise.all(e.map((e=>this._fetcher.strategiesByPairs(e))));n.debug("_syncPairDataBatch strategiesBatches",t),this._chainCache.bulkAddPairs(t.flat()),this._uncachedPairs=[]}catch(e){throw n.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){n.error("Failed to fetch strategies for pair:",e,t,a)}}async _syncEvents(){n.debug("_syncEvents called");const e=async()=>{n.debug("_syncEvents processEvents - new cycle started");try{const e=await this._fetcher.getBlockNumber();if("number"!=typeof e)throw n.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 t=this._chainCache.getLatestBlockNumber();if(n.debug("_syncEvents processEvents - latestBlock (start point for new cycle)",t,"currentBlock",e),e>t){if(await this._detectReorg(e))return n.debug("_syncEvents detected reorg - resetting"),void this._resetSelf();n.debug("_syncEvents fetches events",t+1,e);const a=await this._fetcher.getEvents(t+1,e,this._chunkSize);n.debug("_syncEvents fetched events",a);const i=[];for(const e of a)if("StrategyCreated"===e.type){const t=e.data;this._chainCache.hasCachedPair(t.token0,t.token1)||(n.debug("_syncEvents noticed new pair created",t.token0,t.token1),i.push([t.token0,t.token1]))}this._chainCache.applyEvents(a,e),a.some((e=>"TradingFeePPMUpdated"===e.type))&&(n.debug("_syncEvents noticed at least one default fee update - refetching pair fees for all pairs"),await this._populateFeesData([...await this._fetcher.pairs()])),i.length>0&&(n.debug("_syncEvents noticed at least one new pair created - setting slow poll mode to false"),this._slowPollPairs=!1,n.debug("_syncEvents fetching fees for the new pairs"),await this._populateFeesData(i))}}catch(e){n.error("Error syncing events:",e)}this._setTimeout(e,this._msToWaitBetweenSyncs)};this._setTimeout(e,1)}async _detectReorg(e){n.debug("_detectReorg called");const t=this._chainCache.blocksMetadata,a={};for(const i of t){const{number:t,hash:r}=i;if(t>e)return n.log("reorg detected for block number",t,"larger than current block",e,"with hash",r),!0;try{const e=await this._fetcher.getBlock(t);if(!e||!e.hash||!e.number)return n.error("Failed to fetch block data for block number",t,"- treating as potential reorg"),!0;const s=e.hash;if(r!==s)return n.log("reorg detected for block number",t,"old hash",r,"new hash",s),!0;a[t]={number:i.number,hash:i.hash}}catch(e){return n.error("Error fetching block data for block number",t,":",e),!0}}n.debug("_detectReorg no reorg detected, updating blocks metadata");const i=[];for(let t=0;t<3;t++){const r=e-t;if(a[r])i.push(a[r]);else try{const e=await this._fetcher.getBlock(r);if(!e||!e.number||!e.hash){n.error("Failed to fetch new block data for block number",r,"- skipping this block");continue}i.push({number:e.number,hash:e.hash})}catch(e){n.error("Error fetching new block data for block number",r,":",e,"- skipping this block");continue}}return this._chainCache.blocksMetadata=i,n.debug("_detectReorg updated blocks metadata"),!1}}const o=(e,t,a,i,r)=>{let n;t&&(n=s.fromSerialized(t)),n||(n=new s);const o=new c(e,n,a,i,r);return n.setCacheMissHandler(o.syncPairData.bind(o)),{cache:n,startDataSync:o.startDataSync.bind(o)}};var h=Object.freeze({__proto__:null,ChainCache:s,ChainSync:c,initSyncedCache:o});exports.ChainCache=s,exports.ChainSync=c,exports.index=h,exports.initSyncedCache=o;