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) 15.1 kB
"use strict";var e=require("events"),t=require("./serializers-DfRMJ_-P.cjs"),a=require("./logger-D_Sf8-fo.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]]},h=(e,t)=>i([e,t]);const o=new a.Logger("ChainCache.ts");class d extends e{_strategiesByPair={};_strategiesById={};_ordersByDirectedPair={};_latestBlockNumber=0;_latestTradesByPair={};_latestTradesByDirectedPair={};_blocksMetadata=[];_tradingFeePPMByPair={};_handleCacheMiss;static fromSerialized(e){try{const t=new d;return t._deserialize(e),t}catch(e){o.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)))):o.error("Cached latest block number is not a number, ignoring cache"):o.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._handleCacheMiss&&!this.hasCachedPair(e,t)&&(o.debug("Cache miss for pair",e,t),await this._handleCacheMiss(e,t),o.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]}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=h(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=h(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,r=!1){o.debug("Adding pair with",a.length," strategies to cache",e,t);const s=n(e,t);if(this._strategiesByPair[s])throw new Error(`Pair ${s} already cached`);this._strategiesByPair[s]=a,a.forEach((e=>{this._strategiesById[e.id.toString()]=e,this._addStrategyOrders(e)})),r||(o.debug("Emitting onPairAddedToCache",e,t),this.emit("onPairAddedToCache",c(s)))}addPairFees(e,t,a){o.debug("Adding trading fee to pair",e,t,"fee",a);const r=n(e,t);this._tradingFeePPMByPair[r]=a}applyBatchedUpdates(e,t,a,r,s,i){o.debug("Applying batched updates to cache",{latestBlockNumber:e,latestFeeUpdates:t,latestTrades:a,createdStrategies:r,updatedStrategies:s,deletedStrategies:i});const h=new Set;a.forEach((e=>{this._setLatestTrade(e),h.add(n(e.sourceToken,e.targetToken))})),r.forEach((e=>{this._addStrategy(e),h.add(n(e.token0,e.token1))})),s.forEach((e=>{this._updateStrategy(e),h.add(n(e.token0,e.token1))})),i.forEach((e=>{this._deleteStrategy(e),h.add(n(e.token0,e.token1))})),t.forEach((([e,t,a])=>{this._tradingFeePPMByPair[n(e,t)]=a})),this._setLatestBlockNumber(e),h.size>0&&(o.debug("Emitting onPairDataChanged",h),this.emit("onPairDataChanged",Array.from(h).map(c)))}_setLatestBlockNumber(e){this._latestBlockNumber=e}_setLatestTrade(e){if(!this.hasCachedPair(e.sourceToken,e.targetToken))throw new 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=h(e.sourceToken,e.targetToken);this._latestTradesByDirectedPair[a]=e}_addStrategyOrders(e){for(const t of[[e.token0,e.token1],[e.token1,e.token0]]){const a=h(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=h(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))throw new 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 o.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))throw new 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))throw new 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;constructor(e,t,a=100,r=1e3,s=1e3){this._fetcher=e,this._chainCache=t,this._numOfPairsToBatch=a,this._msToWaitBetweenSyncs=r,this._chunkSize=s}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.applyBatchedUpdates(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 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 setTimeout(e,1e3)}catch(t){l.error("Error while syncing pairs data",t),setTimeout(e,6e4)}};setTimeout(e,1)}async _syncPairDataBatch(){const e=[];for(let t=0;t<this._uncachedPairs.length;t+=this._numOfPairsToBatch)e.push(this._uncachedPairs.slice(t,t+this._numOfPairsToBatch));try{(await Promise.all(e.map((e=>this._fetcher.strategiesByPairs(e))))).flat().forEach((e=>{this._chainCache.addPair(e.pair[0],e.pair[1],e.strategies,!0)})),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()");const a=await this._fetcher.strategiesByPair(e,t);this._chainCache.hasCachedPair(e,t)||this._chainCache.addPair(e,t,a,!1)}_getBlockChunks(e,t,a){const r=[];for(let s=e;s<=t;s+=a){const e=s,i=Math.min(s+a-1,t);r.push([e,i])}return r}async _syncEvents(){l.debug("_syncEvents called");const e=async()=>{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(t>a){if(await this._detectReorg(t))return l.debug("_syncEvents detected reorg - resetting"),this._chainCache.clear(),this._chainCache.applyBatchedUpdates(t,[],[],[],[],[]),this._resetPairsFetching(),void setTimeout(e,1);const r=new Set(this._chainCache.getCachedPairs(!1).map((e=>n(e[0],e[1]))));l.debug("_syncEvents fetches events",a+1,t);const s=this._getBlockChunks(a+1,t,this._chunkSize);l.debug("_syncEvents block chunks",s);const i=[],c=[],h=[],o=[],d=[],g=[];for(const e of s){l.debug("_syncEvents fetches events for chunk",e);const t=await this._fetcher.getLatestStrategyCreatedStrategies(e[0],e[1]),a=await this._fetcher.getLatestStrategyUpdatedStrategies(e[0],e[1]),r=await this._fetcher.getLatestStrategyDeletedStrategies(e[0],e[1]),n=await this._fetcher.getLatestTokensTradedTrades(e[0],e[1]),u=await this._fetcher.getLatestPairTradingFeeUpdates(e[0],e[1]),_=await this._fetcher.getLatestTradingFeeUpdates(e[0],e[1]);i.push(t),c.push(a),h.push(r),o.push(n),d.push(u),g.push(_),l.debug("_syncEvents fetched the following events for chunks",s,{createdStrategiesChunk:t,updatedStrategiesChunk:a,deletedStrategiesChunk:r,tradesChunk:n,feeUpdatesChunk:u,defaultFeeUpdatesChunk:_})}const u=i.flat(),_=c.flat(),y=h.flat(),P=o.flat(),k=d.flat(),f=g.flat().length>0;l.debug("_syncEvents fetched events",u,_,y,P,k,f);const b=[];for(const e of u)this._chainCache.hasCachedPair(e.token0,e.token1)||b.push([e.token0,e.token1]);this._chainCache.applyBatchedUpdates(t,k,P.filter((e=>r.has(n(e.sourceToken,e.targetToken)))),u.filter((e=>r.has(n(e.token0,e.token1)))),_.filter((e=>r.has(n(e.token0,e.token1)))),y.filter((e=>r.has(n(e.token0,e.token1))))),f&&(l.debug("_syncEvents noticed at least one default fee update - refetching pair fees for all pairs"),await this._populateFeesData([...await this._fetcher.pairs()])),b.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(b))}}catch(e){l.error("Error syncing events:",e)}setTimeout(e,this._msToWaitBetweenSyncs)};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 u=(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 _=Object.freeze({__proto__:null,ChainCache:d,ChainSync:g,initSyncedCache:u});exports.ChainCache=d,exports.ChainSync=g,exports.index=_,exports.initSyncedCache=u;