PriceFeeder class
This commit is contained in:
parent
18882f23df
commit
3a6b17f03d
|
@ -40,16 +40,19 @@ function events<T>(emitter: EventEmitter, key: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function coinbase(pair: string): IPriceFeed {
|
export function coinbase(pair: string): IPriceFeed {
|
||||||
|
// TODO: can subscribe to many pairs with one connection
|
||||||
const emitter = new EventEmitter()
|
const emitter = new EventEmitter()
|
||||||
|
|
||||||
const ws = new WebSocket("wss://ws-feed.pro.coinbase.com")
|
const ws = new WebSocket("wss://ws-feed.pro.coinbase.com")
|
||||||
|
|
||||||
|
// "btc:usd" => "BTC-USD"
|
||||||
|
pair = pair.replace(":", "-").toUpperCase()
|
||||||
ws.on("open", () => {
|
ws.on("open", () => {
|
||||||
console.log(`${pair} price feed connected`)
|
console.log(`${pair} price feed connected`)
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "subscribe",
|
type: "subscribe",
|
||||||
product_ids: [pair.replace("/", "-").toUpperCase()],
|
product_ids: [pair],
|
||||||
channels: ["ticker"],
|
channels: ["ticker"],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import fs from "fs"
|
||||||
|
import { Wallet } from "solray"
|
||||||
|
import { config } from "winston"
|
||||||
|
import { AggregatorDeployFile } from "./deploy"
|
||||||
|
import { loadJSONFile } from "./json"
|
||||||
|
import { coinbase } from "./PriceFeed"
|
||||||
|
import { Submitter, SubmitterConfig } from "./Submitter"
|
||||||
|
|
||||||
|
interface IPriceFeederConfig {
|
||||||
|
feeds: {
|
||||||
|
[key: string]: SubmitterConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PriceFeeder {
|
||||||
|
private deployInfo: AggregatorDeployFile
|
||||||
|
private config: IPriceFeederConfig
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
deployInfoFile: string,
|
||||||
|
configFile: string,
|
||||||
|
private wallet: Wallet
|
||||||
|
) {
|
||||||
|
this.deployInfo = loadJSONFile(deployInfoFile)
|
||||||
|
this.config = loadJSONFile(configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
// find aggregators that this wallet can act as oracle
|
||||||
|
this.startAccessibleAggregators()
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAccessibleAggregators() {
|
||||||
|
for (let [name, aggregatorInfo] of Object.entries(
|
||||||
|
this.deployInfo.aggregators
|
||||||
|
)) {
|
||||||
|
const oracleInfo = Object.values(aggregatorInfo.oracles).find(
|
||||||
|
(oracleInfo) => {
|
||||||
|
return oracleInfo.owner.equals(this.wallet.pubkey)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oracleInfo == null) {
|
||||||
|
console.log("no oracle found for:", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceFeed = coinbase(name)
|
||||||
|
const submitter = new Submitter(
|
||||||
|
this.deployInfo.programID,
|
||||||
|
aggregatorInfo.pubkey,
|
||||||
|
oracleInfo.pubkey,
|
||||||
|
this.wallet,
|
||||||
|
priceFeed,
|
||||||
|
{
|
||||||
|
// TODO: errrrr... how do i make this configurable?
|
||||||
|
// don't submit value unless btc changes at least a dollar
|
||||||
|
minValueChangeForNewRound: 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log("start price feeding:", name)
|
||||||
|
submitter.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ import { IPriceFeed } from "./PriceFeed"
|
||||||
// allow oracle to start a new round after this many slots. each slot is about 500ms
|
// allow oracle to start a new round after this many slots. each slot is about 500ms
|
||||||
const MAX_ROUND_STALENESS = 10
|
const MAX_ROUND_STALENESS = 10
|
||||||
|
|
||||||
interface SubmitterConfig {
|
export interface SubmitterConfig {
|
||||||
// won't start a new round unless price changed this much
|
// won't start a new round unless price changed this much
|
||||||
minValueChangeForNewRound: number
|
minValueChangeForNewRound: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ export class AppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
async oracleWallet() {
|
async oracleWallet() {
|
||||||
return walletFromEnv("ADMIN_MNEMONIC", conn)
|
return walletFromEnv("ORACLE_MNEMONIC", conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get aggregatorProgramID() {
|
// get aggregatorProgramID() {
|
||||||
|
|
|
@ -18,7 +18,8 @@ import {
|
||||||
} from "./config"
|
} from "./config"
|
||||||
import FluxAggregator from "./FluxAggregator"
|
import FluxAggregator from "./FluxAggregator"
|
||||||
import { AggregatorConfig, IAggregatorConfig } from "./schema"
|
import { AggregatorConfig, IAggregatorConfig } from "./schema"
|
||||||
// import { AggregatorConfig } from "./schema"
|
import { jsonReplacer, jsonReviver } from "./json"
|
||||||
|
|
||||||
|
|
||||||
interface OracleDeployInfo {
|
interface OracleDeployInfo {
|
||||||
pubkey: PublicKey
|
pubkey: PublicKey
|
||||||
|
@ -47,30 +48,6 @@ const FLUX_AGGREGATOR_SO = path.resolve(
|
||||||
"../build/flux_aggregator.so"
|
"../build/flux_aggregator.so"
|
||||||
)
|
)
|
||||||
|
|
||||||
function jsonReviver(_key: string, val: any) {
|
|
||||||
if (val && typeof val == "object") {
|
|
||||||
if (val["type"] == "PublicKey") {
|
|
||||||
return new PublicKey(val.base58)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
function jsonReplacer(key: string, value: any) {
|
|
||||||
if (value && typeof value != "object") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.constructor == PublicKey) {
|
|
||||||
return {
|
|
||||||
type: "PublicKey",
|
|
||||||
base58: value.toBase58(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Deployer {
|
export class Deployer {
|
||||||
// file backed json state
|
// file backed json state
|
||||||
public setup: AggregatorSetupFile
|
public setup: AggregatorSetupFile
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { PublicKey } from "solray"
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
export function jsonReviver(_key: string, val: any) {
|
||||||
|
if (val && typeof val == "object") {
|
||||||
|
if (val["type"] == "PublicKey") {
|
||||||
|
return new PublicKey(val.base58)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonReplacer(key: string, value: any) {
|
||||||
|
if (value && typeof value != "object") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.constructor == PublicKey) {
|
||||||
|
return {
|
||||||
|
type: "PublicKey",
|
||||||
|
base58: value.toBase58(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadJSONFile<T>(file: string): T {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf8"), jsonReviver)
|
||||||
|
}
|
98
test.ts
98
test.ts
|
@ -1,49 +1,32 @@
|
||||||
import dotenv from "dotenv"
|
import dotenv from "dotenv"
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
import BN from "bn.js"
|
|
||||||
|
|
||||||
import { ProgramAccount, SPLToken, Wallet } from "solray"
|
|
||||||
import { AppContext, conn, network } from "./src/context"
|
import { AppContext, conn, network } from "./src/context"
|
||||||
|
|
||||||
import fs from "fs"
|
|
||||||
|
|
||||||
import { AggregatorConfig } from "./src/schema"
|
|
||||||
import FluxAggregator from "./src/FluxAggregator"
|
|
||||||
|
|
||||||
import * as encoding from "./src/schema"
|
|
||||||
import { Account, AccountInfo, Connection, PublicKey } from "@solana/web3.js"
|
|
||||||
import { coinbase } from "./src/PriceFeed"
|
|
||||||
import { Submitter } from "./src/Submitter"
|
|
||||||
import { Deployer } from "./src/deploy"
|
import { Deployer } from "./src/deploy"
|
||||||
|
import { PriceFeeder } from "./src/PriceFeeder"
|
||||||
|
|
||||||
import { loadAggregatorSetup } from "./src/config"
|
|
||||||
|
|
||||||
import { stateFromJSON } from "./src/state"
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|
||||||
|
console.log({network})
|
||||||
|
const setupFile = `config/setup.${network}.json`
|
||||||
|
const deployFile = `deploy.${network}.json`
|
||||||
|
const feederConfigFile = "feeder.json"
|
||||||
let ctx = new AppContext()
|
let ctx = new AppContext()
|
||||||
let adminWallet = await ctx.adminWallet()
|
let adminWallet = await ctx.adminWallet()
|
||||||
const deployer = new Deployer(
|
let oracleWallet = await ctx.oracleWallet()
|
||||||
`deploy2.${network}.json`,
|
|
||||||
`config/setup.${network}.json`,
|
await conn.requestAirdrop(adminWallet.pubkey, 10 * 1e9)
|
||||||
adminWallet
|
await conn.requestAirdrop(oracleWallet.pubkey, 10 * 1e9)
|
||||||
)
|
|
||||||
|
const deployer = new Deployer(deployFile, setupFile, adminWallet)
|
||||||
|
|
||||||
await deployer.runAll()
|
await deployer.runAll()
|
||||||
|
|
||||||
|
const feeder = new PriceFeeder(deployFile, feederConfigFile, oracleWallet)
|
||||||
|
feeder.start()
|
||||||
console.log("done")
|
console.log("done")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
// let deployer = await ctx.deployer()
|
|
||||||
|
|
||||||
// let oracleWallet = await ctx.oracleWallet()
|
|
||||||
|
|
||||||
// console.log(network)
|
|
||||||
|
|
||||||
// await conn.requestAirdrop(adminWallet.pubkey, 10 * 1e9)
|
|
||||||
// console.log((await conn.getBalance(adminWallet.pubkey)) / 1e9)
|
|
||||||
|
|
||||||
// const spltoken = new SPLToken(adminWallet)
|
// const spltoken = new SPLToken(adminWallet)
|
||||||
// const rewardToken = await deployer.ensure("create reward token", async () => {
|
// const rewardToken = await deployer.ensure("create reward token", async () => {
|
||||||
// return spltoken.initializeMint({
|
// return spltoken.initializeMint({
|
||||||
|
@ -77,59 +60,6 @@ async function main() {
|
||||||
// )
|
// )
|
||||||
|
|
||||||
// console.log(await spltoken.mintInfo(rewardToken.publicKey))
|
// console.log(await spltoken.mintInfo(rewardToken.publicKey))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const N_ORACLES = 4
|
|
||||||
// interface OracleRole {
|
|
||||||
// owner: Account
|
|
||||||
// oracle: PublicKey
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const oracleRoles: OracleRole[] = []
|
|
||||||
|
|
||||||
// for (let i = 0; i < N_ORACLES; i++) {
|
|
||||||
// // TODO: probably put the desired oracles in a config file...
|
|
||||||
// let owner = await deployer.ensure(`create oracle[${i}] owner`, async () => {
|
|
||||||
// return new Account()
|
|
||||||
// })
|
|
||||||
|
|
||||||
// let oracle = await deployer.ensure(
|
|
||||||
// `add oracle[${i}] to btc:usd`,
|
|
||||||
// async () => {
|
|
||||||
// return program.addOracle({
|
|
||||||
// description: "test-oracle",
|
|
||||||
// aggregator: aggregator.publicKey,
|
|
||||||
// aggregatorOwner: adminWallet.account,
|
|
||||||
// oracleOwner: owner.publicKey,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
|
|
||||||
// oracleRoles.push({ owner, oracle: oracle.publicKey })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (const role of oracleRoles) {
|
|
||||||
// // const wallet = Wallet.from
|
|
||||||
// const owner = Wallet.fromAccount(role.owner, conn)
|
|
||||||
// await conn.requestAirdrop(owner.pubkey, 10 * 1e9)
|
|
||||||
// console.log(owner.address, await conn.getBalance(owner.pubkey))
|
|
||||||
|
|
||||||
// const priceFeed = coinbase("BTC/USD")
|
|
||||||
// const submitter = new Submitter(
|
|
||||||
// aggregatorProgram.publicKey,
|
|
||||||
// aggregator.publicKey,
|
|
||||||
// role.oracle,
|
|
||||||
// owner,
|
|
||||||
// priceFeed,
|
|
||||||
// {
|
|
||||||
// // don't submit value unless btc changes at least a dollar
|
|
||||||
// minValueChangeForNewRound: 100,
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
|
|
||||||
// submitter.start()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => console.log(err))
|
main().catch((err) => console.log(err))
|
||||||
|
|
Loading…
Reference in New Issue