-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Stateful Comptroller and price subscribers for delegator (#6)
* Add stateful comptroller * Add StatefulPriceFeed with anchor values * Add PriceData contract * Finish first pass impl of stateful prices sub * Remove CTokens from competitors and fix underlying symbol mapping * Add stateful Coinbase reporter * Extract price data structures from stateful reporter * Rename oracle contracts to their actual names * Direct StatefulPriceFeed updates to the PriceLedger as well * Rename StatefulXXX that deal with prices * Add fancy logging with winston
- Loading branch information
1 parent
ae060f7
commit f7a00ed
Showing
22 changed files
with
933 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
PROVIDER_IPC_PATH= | ||
|
||
SLACK_WEBHOOK= | ||
SLACK_WEBHOOK= | ||
|
||
COINBASE_ENDPOINT= | ||
CB_ACCESS_KEY= | ||
CB_ACCESS_SECRET= | ||
CB_ACCESS_PASSPHRASE= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import crypto from 'crypto'; | ||
import nfetch from 'node-fetch'; | ||
|
||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
const AbiCoder = require('web3-eth-abi'); | ||
/* eslint-enable @typescript-eslint/no-var-requires */ | ||
|
||
interface DecodedMessage { | ||
key: string; | ||
price: string; | ||
timestamp: string; | ||
} | ||
|
||
interface LocalSignature { | ||
hash: string; | ||
timestamp: string; | ||
} | ||
|
||
interface CoinbaseReport { | ||
timestamp: string; | ||
messages: string[]; | ||
signatures: string[]; | ||
prices: { [i: string]: string }; | ||
} | ||
|
||
export default class CoinbaseReporter { | ||
private readonly coinbaseEndpoint: string; | ||
private readonly coinbaseKey: string; | ||
private readonly coinbaseSecret: string; | ||
private readonly coinbasePassphrase: string; | ||
|
||
constructor(coinbaseEndpoint: string, coinbaseKey: string, coinbaseSecret: string, coinbasePassphrase: string) { | ||
this.coinbaseEndpoint = coinbaseEndpoint; | ||
this.coinbaseKey = coinbaseKey; | ||
this.coinbaseSecret = coinbaseSecret; | ||
this.coinbasePassphrase = coinbasePassphrase; | ||
} | ||
|
||
public static decode(message: string): DecodedMessage { | ||
const { | ||
// 0: kind, | ||
1: timestamp, | ||
2: key, | ||
3: price, | ||
} = AbiCoder.decodeParameters(['string', 'uint64', 'string', 'uint64'], message); | ||
|
||
return { | ||
key: key, | ||
price: price, | ||
timestamp: timestamp, | ||
}; | ||
} | ||
|
||
protected async fetchCoinbasePrices(): Promise<CoinbaseReport> { | ||
const path = '/oracle'; | ||
const method = 'GET'; | ||
|
||
const sig = this.localSignature(path, method); | ||
const headers = { | ||
'Content-Type': 'application/json', | ||
Accept: 'application/json', | ||
'CB-ACCESS-KEY': this.coinbaseKey, | ||
'CB-ACCESS-SIGN': sig.hash, | ||
'CB-ACCESS-TIMESTAMP': sig.timestamp, | ||
'CB-ACCESS-PASSPHRASE': this.coinbasePassphrase, | ||
}; | ||
|
||
const res = await nfetch(this.coinbaseEndpoint + path, { | ||
method: method, | ||
headers: headers, | ||
}); | ||
return res.json(); | ||
} | ||
|
||
private localSignature(path = '/oracle', method = 'GET', body = ''): LocalSignature { | ||
const timestamp = (Date.now() / 1000).toFixed(0); | ||
const prehash = timestamp + method.toUpperCase() + path + body; | ||
const hash = crypto | ||
.createHmac('sha256', Buffer.from(this.coinbaseSecret, 'base64')) | ||
.update(prehash) | ||
.digest('base64'); | ||
|
||
return { | ||
hash: hash, | ||
timestamp: timestamp, | ||
}; | ||
} | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import { Big } from '@goldenagellc/web3-blocks'; | ||
|
||
import { CoinbaseKey } from './types/CoinbaseKeys'; | ||
import { CTokenSymbol } from './types/CTokens'; | ||
import IPrice from './types/IPrice'; | ||
import IPriceRange from './types/IPriceRange'; | ||
|
||
interface PostableDatum { | ||
key: CoinbaseKey; | ||
message: string; | ||
signature: string; | ||
} | ||
|
||
type TimestampMap = { [i: string]: PostableDatum }; | ||
|
||
const USD_VALUE: Big = Big('1000000'); | ||
const SAI_PER_ETH = '0.005285'; | ||
|
||
export default class PriceLedger { | ||
private readonly prices: { [_ in CoinbaseKey]: IPriceRange | null } = { | ||
BAT: null, | ||
COMP: null, | ||
DAI: null, | ||
ETH: null, | ||
REP: null, | ||
UNI: null, | ||
BTC: null, | ||
ZRX: null, | ||
}; | ||
private readonly priceHistories: { readonly [_ in CoinbaseKey]: IPrice[] } = { | ||
BAT: [], | ||
COMP: [], | ||
DAI: [], | ||
ETH: [], | ||
REP: [], | ||
UNI: [], | ||
BTC: [], | ||
ZRX: [], | ||
}; | ||
private readonly postableData: { readonly [_ in CoinbaseKey]: TimestampMap } = { | ||
BAT: {}, | ||
COMP: {}, | ||
DAI: {}, | ||
ETH: {}, | ||
REP: {}, | ||
UNI: {}, | ||
BTC: {}, | ||
ZRX: {}, | ||
}; | ||
|
||
public get summaryText(): string { | ||
return this.summaryTextForAll(Object.keys(this.prices) as CoinbaseKey[]); | ||
} | ||
|
||
public summaryTextForAll(keys: CoinbaseKey[]): string { | ||
const texts: string[] = []; | ||
keys.forEach((key) => { | ||
const text = this.summaryTextFor(key); | ||
if (text !== null) texts.push(text); | ||
}); | ||
return texts.join('\n'); | ||
} | ||
|
||
public summaryTextFor(key: CoinbaseKey): string | null { | ||
const price = this.prices[key]; | ||
if (price === null) return null; | ||
|
||
const min = price.min.value.div('1e+6').toFixed(2); | ||
const max = price.max.value.div('1e+6').toFixed(2); | ||
|
||
const now = Date.now() / 1000; | ||
const minAge = ((now - Number(price.min.timestamp)) / 60).toFixed(0); | ||
const maxAge = ((now - Number(price.max.timestamp)) / 60).toFixed(0); | ||
|
||
return `*${key}:*\n\tmin: $${min} (${minAge} min ago)\n\tmax: $${max} (${maxAge} min ago)`; | ||
} | ||
|
||
public getPrices(symbol: CTokenSymbol): { min: Big | null; max: Big | null } { | ||
switch (symbol) { | ||
case 'cBAT': | ||
return { | ||
min: this.prices.BAT?.min.value, | ||
max: this.prices.BAT?.max.value, | ||
}; | ||
case 'cCOMP': | ||
return { | ||
min: this.prices.COMP?.min.value, | ||
max: this.prices.COMP?.max.value, | ||
}; | ||
case 'cDAI': | ||
return { | ||
min: this.prices.DAI?.min.value, | ||
max: this.prices.DAI?.max.value, | ||
}; | ||
case 'cETH': | ||
return { | ||
min: this.prices.ETH?.min.value, | ||
max: this.prices.ETH?.max.value, | ||
}; | ||
case 'cREP': | ||
return { | ||
min: this.prices.REP?.min.value, | ||
max: this.prices.REP?.max.value, | ||
}; | ||
case 'cSAI': | ||
return { | ||
min: this.prices.ETH?.min.value.mul(SAI_PER_ETH), | ||
max: this.prices.ETH?.max.value.mul(SAI_PER_ETH), | ||
}; | ||
case 'cUNI': | ||
return { | ||
min: this.prices.UNI?.min.value, | ||
max: this.prices.UNI?.max.value, | ||
}; | ||
case 'cUSDC': | ||
case 'cUSDT': | ||
return { | ||
min: USD_VALUE, | ||
max: USD_VALUE, | ||
}; | ||
case 'cWBTC': | ||
return { | ||
min: this.prices.BTC?.min.value, | ||
max: this.prices.BTC?.max.value, | ||
}; | ||
case 'cZRX': | ||
return { | ||
min: this.prices.ZRX?.min.value, | ||
max: this.prices.ZRX?.max.value, | ||
}; | ||
} | ||
} | ||
|
||
public append(key: CoinbaseKey, price: IPrice, message: string, signature: string): boolean { | ||
// Check to make sure price is actually new | ||
const len = this.priceHistories[key].length; | ||
if (len > 0 && this.priceHistories[key][len - 1].timestamp === price.timestamp) return false; | ||
|
||
this.priceHistories[key].push(price); | ||
this.postableData[key][price.timestamp!] = { | ||
key: key, | ||
message: message, | ||
signature: signature, | ||
}; | ||
|
||
return this.updateMinMax(key, price); | ||
} | ||
|
||
private updateMinMax(key: CoinbaseKey, price: IPrice): boolean { | ||
let didUpdate = false; | ||
|
||
const priceOld = this.prices[key]; | ||
if (priceOld === null) { | ||
this.prices[key] = { | ||
min: price, | ||
max: price, | ||
}; | ||
didUpdate = true; | ||
} else if (price.value.lte(priceOld.min.value)) { | ||
this.prices[key]!.min = price; | ||
didUpdate = true; | ||
} else if (price.value.gte(priceOld.max.value)) { | ||
this.prices[key]!.max = price; | ||
didUpdate = true; | ||
} | ||
|
||
return didUpdate; | ||
} | ||
|
||
private resetPrices(key: CoinbaseKey, maskToTimestamp: string): void { | ||
this.prices[key] = null; | ||
this.priceHistories[key].forEach((price) => { | ||
if (Number(price.timestamp) < Number(maskToTimestamp)) return; | ||
this.updateMinMax(key, price); | ||
}); | ||
} | ||
|
||
public cleanHistory(key: CoinbaseKey, delToTimestamp: string, maskToTimestamp: string): void { | ||
let i: number; | ||
for (i = 0; i < this.priceHistories[key].length; i += 1) { | ||
const ts = this.priceHistories[key][i].timestamp; | ||
if (Number(ts) >= Number(delToTimestamp)) break; | ||
|
||
delete this.postableData[key][delToTimestamp]; | ||
} | ||
this.priceHistories[key].splice(0, i); | ||
|
||
this.resetPrices(key, maskToTimestamp); | ||
} | ||
} |
Oops, something went wrong.