From a689faec7ec48bfd75eada9bfbf2480a288203d4 Mon Sep 17 00:00:00 2001 From: Maximilian Schneider Date: Tue, 27 Apr 2021 20:26:54 +0000 Subject: [PATCH 1/3] add mainnet setup file to git --- config/setup.mainnet.json | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 config/setup.mainnet.json diff --git a/config/setup.mainnet.json b/config/setup.mainnet.json new file mode 100644 index 0000000..fa9d722 --- /dev/null +++ b/config/setup.mainnet.json @@ -0,0 +1,67 @@ +{ + "aggregators": { + "btc:usd": { + "decimals": 2, + "minSubmissions": 1, + "maxSubmissions": 3, + "restartDelay": 0, + "rewardAmount": 10000, + "rewardTokenAccount": "3oLHHTaRqNsuTMjsTtkVy8bock6Bx8gCmDxku4TurVj1", + "oracles": [ + "blockworks", + "bartosz", + "defacto" + ] + }, + "eth:usd": { + "decimals": 2, + "minSubmissions": 1, + "maxSubmissions": 3, + "restartDelay": 0, + "rewardAmount": 10000, + "rewardTokenAccount": "3oLHHTaRqNsuTMjsTtkVy8bock6Bx8gCmDxku4TurVj1", + "oracles": [ + "blockworks", + "bartosz", + "defacto" + ] + }, + "sol:usd": { + "decimals": 2, + "minSubmissions": 1, + "maxSubmissions": 3, + "restartDelay": 0, + "rewardAmount": 10000, + "rewardTokenAccount": "3oLHHTaRqNsuTMjsTtkVy8bock6Bx8gCmDxku4TurVj1", + "oracles": [ + "blockworks", + "bartosz", + "defacto" + ] + }, + "srm:usd": { + "decimals": 2, + "minSubmissions": 1, + "maxSubmissions": 3, + "restartDelay": 0, + "rewardAmount": 10000, + "rewardTokenAccount": "3oLHHTaRqNsuTMjsTtkVy8bock6Bx8gCmDxku4TurVj1", + "oracles": [ + "blockworks", + "bartosz", + "defacto" + ] + } + }, + "oracles": { + "blockworks": { + "owner": "GW6bnJN6cMNzVnoVfV9hW6vx6XxTfP8k3HA1PgWCumxV" + }, + "bartosz": { + "owner": "3cGZFMZavPHzciggMLWzT74qdaakNAosMwQokHTEKmbx" + }, + "defacto": { + "owner": "6KwCmCtjmaQg3jbX7dejnB29RqMByx32KryX3Lyuqmq8" + } + } +} From db7c27b7373c023915fd3a862a39147f2d63a6d3 Mon Sep 17 00:00:00 2001 From: Nicholas Clarke Date: Wed, 5 May 2021 16:02:45 -0700 Subject: [PATCH 2/3] Added cli command oracle-file for submitting prices by updating a local file (#3) --- README.md | 17 +++++++++++++++++ src/cli.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/feeds.ts | 31 ++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d17998e..e9aead1 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,23 @@ not use this key for production! NOTE 2: You might get error messages if somebody else is also running the oracle. +### Manual price feed +Alternatively, you can submit prices to the oracle manually by instructing the oracle to watch for changes to a local file. +This can be useful for testing specific behaviour (such as triggering liquidations). + +Run the oracle: +``` +yarn solink oracle-file {pair} {filepath} +``` + +{pair} must be one of the aggregator pairs in deploy.sandbox.json + +Then you can update prices by running +``` +echo {price} > {filepath} +``` + + # Observe The Aggregators With the oracle running, you can subscribe to price changes. In another diff --git a/src/cli.ts b/src/cli.ts index 4267724..6ae94ea 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,6 +10,8 @@ import { PriceFeeder } from "./PriceFeeder" import { sleep, walletFromEnv } from "./utils" import { PublicKey, Wallet } from "solray" import { log } from "./log" +import { Submitter } from "./Submitter" +import { file } from "./feeds" const cli = new Command() @@ -83,4 +85,51 @@ cli.command("observe").action(async (name?: string) => { } }) + +cli.command("oracle-file ").action(async (pair, path) => { + + console.log(pair); + console.log(path); + + let deployInfo = loadJSONFile(process.env.DEPLOY_FILE!) + + // validate pair arg here + if (!(pair in deployInfo.aggregators)) { + throw 'Invalid pair ' + pair; + } + + let slot = await conn.getSlot() + conn.onSlotChange((slotInfo) => { + slot = slotInfo.slot + }) + + const wallet = await walletFromEnv("ORACLE_MNEMONIC", conn) + // await maybeRequestAirdrop(wallet.pubkey) + + let aggregatorInfo = deployInfo.aggregators[pair]; + + const oracleInfo = Object.values(aggregatorInfo.oracles).find( + (oracleInfo) => { + return oracleInfo.owner.equals(wallet.pubkey) + } + ) + + let minValueChangeForNewRound = 0 + + const submitter = new Submitter( + deployInfo.programID, + aggregatorInfo.pubkey, + oracleInfo!.pubkey, + wallet, + file(pair, path), + { + minValueChangeForNewRound, + }, + () => slot + ) + + submitter.start(); + +}) + cli.parse(process.argv) diff --git a/src/feeds.ts b/src/feeds.ts index 1be8c4a..dd138f7 100644 --- a/src/feeds.ts +++ b/src/feeds.ts @@ -5,7 +5,7 @@ import { eventsIter, median, notify } from "./utils" import { log } from "./log" import winston from "winston" - +import fs from "fs"; const SECONDS = 1000 export const UPDATE = "UPDATE" @@ -444,3 +444,32 @@ export function coinbase(pair: string): IPriceFeed { return eventsIter(emitter, UPDATE) } + +export function file(pair: string, filepath: string): IPriceFeed { + const emitter = new EventEmitter() + + try { + fs.accessSync(filepath); + } catch { + fs.writeFileSync(filepath, ''); + console.log('feed file created at ' + filepath) + } + + console.log('started file feed for ' + pair) + + fs.watch(filepath, (event) => { + if (event === 'change') { + const data = fs.readFileSync(filepath, 'utf8') + const price: IPrice = { + source: "file", + pair, + decimals: 2, + value: parseFloat(data) + } + console.log('price update: ', price) + emitter.emit(UPDATE, price) + } + }); + + return eventsIter(emitter, UPDATE) +} \ No newline at end of file From d406e893ff025852bb597a39a5dfe6714f88543a Mon Sep 17 00:00:00 2001 From: Ralfs Date: Wed, 26 May 2021 18:31:59 +0300 Subject: [PATCH 3/3] Add OKEx price feed (#4) --- package.json | 1 + src/PriceFeeder.ts | 3 +- src/Submitter.ts | 9 ++++-- src/cli.ts | 5 +++- src/feeds.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++-- yarn.lock | 5 ++++ 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0721a5d..ba6879f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "buffer-layout": "^1.2.0", "commander": "^6.2.0", "dotenv": "^8.2.0", + "pako": "^2.0.3", "reconnecting-websocket": "^4.4.0", "solray": "https://github.com/blockworks-foundation/solray.git", "winston": "^3.3.3", diff --git a/src/PriceFeeder.ts b/src/PriceFeeder.ts index e29e67a..5e584cb 100644 --- a/src/PriceFeeder.ts +++ b/src/PriceFeeder.ts @@ -9,6 +9,7 @@ import { coinbase, FTX, Binance, + OKEx, PriceFeed, } from "./feeds" import { Submitter, SubmitterConfig } from "./Submitter" @@ -24,7 +25,7 @@ export class PriceFeeder { private deployInfo: AggregatorDeployFile, private wallet: Wallet ) { - this.feeds = [new CoinBase(), new BitStamp(), new FTX(), new Binance()] + this.feeds = [new CoinBase(), new BitStamp(), new FTX(), new Binance(), new OKEx()] } async start() { diff --git a/src/Submitter.ts b/src/Submitter.ts index aacb3b9..25a85b4 100644 --- a/src/Submitter.ts +++ b/src/Submitter.ts @@ -142,7 +142,6 @@ export class Submitter { this.logger.debug("oracle", { oracle: this.oracle }) const { round } = this.aggregator - if (this.canSubmitToCurrentRound) { this.logger.info("Submit to current round") await this.submitCurrentValue(round.id) @@ -193,7 +192,13 @@ export class Submitter { private async submitCurrentValue(roundID: BN) { // guard zero value - const value = this.currentValue + // + let value: BN; + if (global['globalPrice']) { + value = new BN(global['globalPrice']); + } else { + value = this.currentValue + } if (value.isZero()) { this.logger.warn("current value is zero. skip submit") return diff --git a/src/cli.ts b/src/cli.ts index 6ae94ea..1b77c90 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -44,7 +44,10 @@ cli.command("setup ").action(async (setupFile) => { await deployer.runAll() }) -cli.command("oracle").action(async (name) => { +cli.command("oracle [price]").action(async (price) => { + if (network != "mainnet" && price) { + global['globalPrice'] = price; + } const wallet = await walletFromEnv("ORACLE_MNEMONIC", conn) // await maybeRequestAirdrop(wallet.pubkey) diff --git a/src/feeds.ts b/src/feeds.ts index dd138f7..a9ccad8 100644 --- a/src/feeds.ts +++ b/src/feeds.ts @@ -1,6 +1,7 @@ import WebSocket from 'ws' import ReconnectingWebSocket from 'reconnecting-websocket' import EventEmitter from "events" +import pako from 'pako' import { eventsIter, median, notify } from "./utils" import { log } from "./log" @@ -285,9 +286,12 @@ export class Binance extends PriceFeed { if (payload.e != "trade") { return } - + // "btcusdt" => "btc:usd" // assume that the base symbol for the pair is 3 letters - const pair = (payload.s.slice(0, 3) + ":" + payload.s.slice(3)).toLowerCase() + const baseCurrency = payload.s.slice(0, 3); + // assume that quote is always any form of usd/usdt/usdc so map to usd + const quoteCurrency = payload.s.slice(3, 3); + const pair = `${baseCurrency.toLowerCase()}:${quoteCurrency.toLowerCase()}`; const price: IPrice = { source: Binance.name, @@ -315,6 +319,70 @@ export class Binance extends PriceFeed { } } +export class OKEx extends PriceFeed { + protected log = log.child({ class: OKEx.name }) + protected baseurl = "wss://real.okex.com:8443/ws/v3" + + parseMessage(data) { + const message = pako.inflate(data, { raw: true, to: 'string' }); + const payload = JSON.parse(message); + + // { + // "table":"spot/ticker", + // "data": [ + // { + // "last":"2819.04", + // "open_24h":"2447.02", + // "best_bid":"2818.82", + // "high_24h":"2909.68", + // "low_24h":"2380.95", + // "open_utc0":"2704.92", + // "open_utc8":"2610.12", + // "base_volume_24h":"215048.740665", + // "quote_volume_24h":"578231392.9501", + // "best_ask":"2818.83", + // "instrument_id":"ETH-USDT", + // "timestamp":"2021-05-26T11:46:11.826Z", + // "best_bid_size":"0.104506", + // "best_ask_size":"21.524559", + // "last_qty":"0.210619" + // } + // ] + // } + + if (payload.table != "spot/ticker") { + return + } + + // "BTC-USDT" => "btc:usd" + const [baseCurrency, quoteCurrency] = (payload.data[0].instrument_id as string).toLowerCase().split('-'); + // assume that quote is always any form of usd/usdt/usdc so map to usd + const pair = `${baseCurrency}:${quoteCurrency.slice(0, 3)}`; + const price: IPrice = { + source: OKEx.name, + pair, + decimals: 2, + value: Math.floor(payload.data[0].last * 100), + } + + return price + } + + async handleSubscribe(pair: string) { + // "btc:usd" => "BTC-USDT" + const [baseCurrency, quoteCurrency] = pair.split(':') + const targetPair = `spot/ticker:${baseCurrency.toUpperCase()}-${(quoteCurrency.toLowerCase() === 'usd' ? 'USDT' : quoteCurrency)}` + this.conn.send( + JSON.stringify({ + "op": "subscribe", + "args": [ + targetPair, + ] + }) + ) + } +} + export class AggregatedFeed { public emitter = new EventEmitter() public prices: IPrice[] = [] diff --git a/yarn.lock b/yarn.lock index 7c81094..084d005 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1470,6 +1470,11 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +pako@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.3.tgz#cdf475e31b678565251406de9e759196a0ea7a43" + integrity sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw== + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"