import { EventData } from 'web3-eth-contract' ; import Web3 from 'web3' ; import winston from 'winston' ; import { Big } from '@goldenagellc/web3-blocks' ; import { OpenOraclePriceData } from './contracts/OpenOraclePriceData' ; import { UniswapAnchoredView } from './contracts/UniswapAnchoredView' ; import { CoinbaseKey , coinbaseKeyMap } from './types/CoinbaseKeys' ; import { CTokens , cTokenUnderlyingDecimals as decimals } from './types/CTokens' ; import IPrice from './types/IPrice' ; import PriceLedger from './PriceLedger' ; interface IOnChainPrice extends IPrice { block : number ; logIndex : number ; } export default class StatefulPricesOnChain { private readonly provider : Web3 ; private readonly ledger : PriceLedger ; private readonly openOraclePriceData : OpenOraclePriceData ; private readonly uniswapAnchoredView : UniswapAnchoredView ; private prices : { [ _ in CoinbaseKey ] : IOnChainPrice [ ] ; } = { BAT : [ ] , COMP : [ ] , DAI : [ ] , ETH : [ ] , REP : [ ] , UNI : [ ] , BTC : [ ] , ZRX : [ ] , LINK : [ ] , } ; constructor ( provider , ledger , openOraclePriceData , uniswapAnchoredView ) { this . provider = provider ; this . ledger = ledger ; this . openOraclePriceData = openOraclePriceData ; this . uniswapAnchoredView = uniswapAnchoredView ; } public async init ( ) { const block = await this . provider . eth . getBlockNumber ( ) ; await Promise . all ( this . fetchPrices ( block ) ) ; this . subscribeToPrices ( block ) ; } private fetchPrices ( block ) { return Object . keys ( coinbaseKeyMap ) . map ( async ( key ) => { const knownKey = key as CoinbaseKey ; const symbol = coinbaseKeyMap [ knownKey ] ; const price = await this . uniswapAnchoredView . getUnderlyingPrice ( CTokens [ symbol ] ) ( this . provider , block ) ; this . prices [ knownKey ] . push ( { value : price . div ( `1e+${ ( 36 - 6 - decimals [ symbol ] ) . toFixed ( 0 ) }` ) , timestamp : '0' , block : block , logIndex : 0 , } ) ; } ) ; } private subscribeToPrices ( block ) { this . openOraclePriceData . bindTo ( this . provider ) . subscribeTo . Write ( block ) . on ( 'connected' , ( id ) => { console . log ( `StatefulPriceFeed: Bound prices to ${ id }` ) ; } ) . on ( 'data' , ( ev ) => { if ( ! Object . keys ( coinbaseKeyMap ) . includes ( ev . returnValues . key ) ) return ; const knownKey = ev . returnValues . key as CoinbaseKey ; const newPrice = { value : new Big ( ev . returnValues . value ) , timestamp : ev . returnValues . timestamp , block : ev . blockNumber , logIndex : ev . logIndex , } ; this . prices [ knownKey ] . push ( newPrice ) ; this . prices [ knownKey ] . sort ( ( a , b ) => b . block - a . block ) ; const idx = this . prices [ knownKey ] . findIndex ( ( p ) => newPrice . block - p . block > 12 ) ; if ( idx !== - 1 ) this . prices [ knownKey ] . splice ( Math . max ( idx , 2 ) ) ; const summaryBefore = this . ledger . summaryTextFor ( knownKey ) ?. replace ( knownKey , 'Before' ) ; this . propogateToLedger ( knownKey ) ; const summaryAfter = this . ledger . summaryTextFor ( knownKey ) ?. replace ( knownKey , 'After' ) ; winston . info ( `📈 ${ knownKey } price posted to chain!\n${ summaryBefore }\n${ summaryAfter }` ) ; } ) . on ( 'changed' , ( ev ) => { if ( ! Object . keys ( coinbaseKeyMap ) . includes ( ev . returnValues . key ) ) return ; const knownKey = ev . returnValues . key as CoinbaseKey ; const idx = this . prices [ knownKey ] . findIndex ( ( p ) => p . block === ev . blockNumber && p . logIndex === ev . logIndex ) ; if ( idx !== - 1 ) this . prices [ knownKey ] . splice ( idx , 1 ) ; const summaryBefore = this . ledger . summaryTextFor ( knownKey ) ?. replace ( knownKey , 'Before' ) ; this . propogateToLedger ( knownKey ) ; const summaryAfter = this . ledger . summaryTextFor ( knownKey ) ?. replace ( knownKey , 'After' ) ; winston . info ( `⚠️ ${ knownKey } price suffered chain reorganization!\n${ summaryBefore }\n${ summaryAfter }` ) ; } ) . on ( 'error' , console . error ) ; } private propogateToLedger ( key ) { const len = this . prices [ key ] . length ; const ledgerIsEmpty = this . ledger . cleanHistory ( key , this . prices [ key ] [ len - 1 ] . timestamp , this . prices [ key ] [ 0 ] . timestamp ) ; if ( ledgerIsEmpty ) this . ledger . append ( key , { value : this . prices [ key ] [ 0 ] . value , timestamp : '0' , } , '' , '' ) ; } }