implement deployer class

This commit is contained in:
De Facto 2021-02-19 18:35:30 +08:00
parent 9c6199e270
commit e223511bc3
8 changed files with 528 additions and 201 deletions

View File

@ -13,6 +13,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@ltd/j-toml": "^1.6.0",
"@solana/web3.js": "^0.90.5", "@solana/web3.js": "^0.90.5",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"commander": "^6.2.0", "commander": "^6.2.0",

View File

@ -28,7 +28,7 @@ import { decodeOracleInfo } from "./utils"
// @ts-ignore // @ts-ignore
// import BufferLayout from "buffer-layout"; // import BufferLayout from "buffer-layout";
import { schema } from "./schema" import { AggregatorConfig, IAggregatorConfig, schema } from "./schema"
import * as encoding from "./schema" import * as encoding from "./schema"
import { deserialize, serialize } from "borsh" import { deserialize, serialize } from "borsh"
import { conn } from "./context" import { conn } from "./context"
@ -51,7 +51,7 @@ export const SubmissionLayout = BufferLayout.struct([
]) ])
interface InitializeParams { interface InitializeParams {
config: encoding.AggregatorConfig config: IAggregatorConfig
owner: Account owner: Account
} }
@ -90,15 +90,17 @@ interface SubmitParams {
} }
interface WithdrawParams { interface WithdrawParams {
aggregator: PublicKey accounts: {
// withdraw to aggregator: PublicKey
receiver: PublicKey
// withdraw amount faucet: { write: PublicKey },
amount: bigint faucetOwner: PublicKey,
tokenAccount: PublicKey oracle: { write: PublicKey },
tokenOwner: PublicKey oracleOwner: Account,
// signer receiver: { write: PublicKey },
authority: Account }
faucetOwnerSeed: Buffer
} }
interface WithdrawInstructionParams extends WithdrawParams {} interface WithdrawInstructionParams extends WithdrawParams {}
@ -115,7 +117,9 @@ export default class FluxAggregator extends BaseProgram {
const answer_submissions = new Account() const answer_submissions = new Account()
const round_submissions = new Account() const round_submissions = new Account()
const input = encoding.Initialize.serialize({ config: params.config }) const input = encoding.Initialize.serialize({
config: new AggregatorConfig(params.config),
})
await this.sendTx( await this.sendTx(
[ [
@ -182,96 +186,95 @@ export default class FluxAggregator extends BaseProgram {
return oracle return oracle
} }
public async oracleInfo(pubkey: PublicKey) { // public async oracleInfo(pubkey: PublicKey) {
const info = await this.conn.getAccountInfo(pubkey) // const info = await this.conn.getAccountInfo(pubkey)
return decodeOracleInfo(info) // return decodeOracleInfo(info)
} // }
public async removeOracle(params: RemoveOracleParams): Promise<void> { // public async removeOracle(params: RemoveOracleParams): Promise<void> {
await this.sendTx( // await this.sendTx(
[this.removeOracleInstruction(params)], // [this.removeOracleInstruction(params)],
[this.account, params.authority || this.wallet.account] // [this.account, params.authority || this.wallet.account]
) // )
} // }
private removeOracleInstruction( // private removeOracleInstruction(
params: RemoveOracleInstructionParams // params: RemoveOracleInstructionParams
): TransactionInstruction { // ): TransactionInstruction {
const { authority, aggregator, oracle } = params // const { authority, aggregator, oracle } = params
const layout = BufferLayout.struct([ // const layout = BufferLayout.struct([
BufferLayout.u8("instruction"), // BufferLayout.u8("instruction"),
BufferLayout.blob(32, "oracle"), // BufferLayout.blob(32, "oracle"),
]) // ])
return this.instructionEncode( // return this.instructionEncode(
layout, // layout,
{ // {
instruction: 2, // remove oracle instruction // instruction: 2, // remove oracle instruction
oracle: oracle.toBuffer(), // oracle: oracle.toBuffer(),
}, // },
[ // [
// // //
{ write: aggregator }, // { write: aggregator },
authority || this.wallet.account, // authority || this.wallet.account,
] // ]
) // )
} // }
public async submit(params: SubmitParams): Promise<void> { public async submit(params: SubmitParams): Promise<void> {
const input = encoding.Submit.serialize(params) const input = encoding.Submit.serialize(params)
let auths = [ let auths = [SYSVAR_CLOCK_PUBKEY, ...Object.values(params.accounts)]
SYSVAR_CLOCK_PUBKEY,
...Object.values(params.accounts),
]
await this.sendTx( await this.sendTx(
[ [this.instruction(input, auths)],
this.instruction(input, auths),
],
[this.account, params.accounts.oracle_owner] [this.account, params.accounts.oracle_owner]
) )
} }
public async withdraw(params: WithdrawParams): Promise<void> { public async withdraw(params: WithdrawParams): Promise<void> {
const input = encoding.Withdraw.serialize(params)
let auths = [SPLToken.programID, ...Object.values(params.accounts)]
await this.sendTx( await this.sendTx(
[this.withdrawInstruction(params)], [this.instruction(input, auths)],
[this.account, params.authority] [this.account, params.accounts.oracleOwner]
) )
} }
private withdrawInstruction( // private withdrawInstruction(
params: WithdrawInstructionParams // params: WithdrawInstructionParams
): TransactionInstruction { // ): TransactionInstruction {
const { // const {
aggregator, // aggregator,
receiver, // receiver,
amount, // amount,
tokenOwner, // tokenOwner,
tokenAccount, // tokenAccount,
authority, // authority,
} = params // } = params
const layout = BufferLayout.struct([ // const layout = BufferLayout.struct([
BufferLayout.u8("instruction"), // BufferLayout.u8("instruction"),
uint64("amount"), // uint64("amount"),
]) // ])
return this.instructionEncode( // return this.instructionEncode(
layout, // layout,
{ // {
instruction: 4, // withdraw instruction // instruction: 4, // withdraw instruction
amount: u64LEBuffer(amount), // amount: u64LEBuffer(amount),
}, // },
[ // [
{ write: aggregator }, // { write: aggregator },
{ write: tokenAccount }, // { write: tokenAccount },
{ write: receiver }, // { write: receiver },
SPLToken.programID, // SPLToken.programID,
tokenOwner, // tokenOwner,
{ write: authority }, // { write: authority },
] // ]
) // )
} // }
} }

View File

@ -28,6 +28,7 @@ interface SubmitterConfig {
export class Submitter { export class Submitter {
public aggregator!: Aggregator public aggregator!: Aggregator
public oracle!: Oracle
public roundSubmissions!: Submissions public roundSubmissions!: Submissions
public answerSubmissions!: Submissions public answerSubmissions!: Submissions
public program: FluxAggregator public program: FluxAggregator
@ -40,7 +41,7 @@ export class Submitter {
public oraclePK: PublicKey, public oraclePK: PublicKey,
private oracleOwnerWallet: Wallet, private oracleOwnerWallet: Wallet,
private priceFeed: IPriceFeed, private priceFeed: IPriceFeed,
private cfg: SubmitterConfig, private cfg: SubmitterConfig
) { ) {
this.program = new FluxAggregator(this.oracleOwnerWallet, programID) this.program = new FluxAggregator(this.oracleOwnerWallet, programID)
@ -66,6 +67,10 @@ export class Submitter {
await Promise.all([this.observeAggregatorState(), this.observePriceFeed()]) await Promise.all([this.observeAggregatorState(), this.observePriceFeed()])
} }
public async withdrawRewards() {
}
private async observeAggregatorState() { private async observeAggregatorState() {
conn.onAccountChange(this.aggregatorPK, async (info) => { conn.onAccountChange(this.aggregatorPK, async (info) => {
this.aggregator = Aggregator.deserialize(info.data) this.aggregator = Aggregator.deserialize(info.data)
@ -96,9 +101,13 @@ export class Submitter {
this.currentValue = new BN(price.value) this.currentValue = new BN(price.value)
const valueDiff = this.aggregator.answer.median.sub(this.currentValue).abs() const valueDiff = this.aggregator.answer.median
if(valueDiff.lten(this.cfg.minValueChangeForNewRound)) { .sub(this.currentValue)
this.logger.debug("price did not change enough to start a new round", { diff: valueDiff.toNumber()}); .abs()
if (valueDiff.lten(this.cfg.minValueChangeForNewRound)) {
this.logger.debug("price did not change enough to start a new round", {
diff: valueDiff.toNumber(),
})
continue continue
} }
@ -110,6 +119,9 @@ export class Submitter {
// TODO: make it possible to be triggered by chainlink task // TODO: make it possible to be triggered by chainlink task
// TODO: If from chainlink node, update state before running // TODO: If from chainlink node, update state before running
this.oracle = await Oracle.load(this.oraclePK)
this.logger.debug("oracle", { oracle: this.oracle })
const { round } = this.aggregator const { round } = this.aggregator
if (this.canSubmitToCurrentRound) { if (this.canSubmitToCurrentRound) {

59
src/config.ts Normal file
View File

@ -0,0 +1,59 @@
import toml from "@ltd/j-toml"
import fs from "fs"
import { Oracle } from "./schema"
function loadJSON(file: string): any {
return JSON.parse(fs.readFileSync(file, "utf8"))
}
const aggregatorConfigDefaults = {
decimals: 0,
minSubmissions: 0,
maxSubmissions: 1,
restartDelay: 0,
rewardAmount: 0,
}
export function loadAggregatorSetup(file: string): AggregatorSetupFile {
let obj: AggregatorSetupFile = loadJSON(file)
for (let key of Object.keys(obj.aggregators)) {
obj.aggregators[key] = {
...aggregatorConfigDefaults,
...obj.aggregators[key],
}
}
return obj
}
export interface OracleConfig {
owner: string
}
export interface AggregatorSetupConfig {
decimals: number
minSubmissions: number
maxSubmissions: number
restartDelay: number
rewardAmount: number
rewardTokenAccount?: string
oracles?: string[]
}
export interface AggregatorSetupFile {
programID: string
aggregators: {
[key: string]: AggregatorSetupConfig
}
oracles: {
[key: string]: OracleConfig
}
}
// //
// export interface DeployManifest {
// programID:
// }

191
src/deploy.ts Normal file
View File

@ -0,0 +1,191 @@
import { stateFromJSON } from "./state"
import fs from "fs"
import path from "path"
import {
Account,
BPFLoader,
ProgramAccount,
PublicKey,
SPLToken,
Wallet,
} from "solray"
import {
AggregatorSetupFile,
AggregatorSetupConfig,
loadAggregatorSetup,
OracleConfig,
} from "./config"
import FluxAggregator from "./FluxAggregator"
import { AggregatorConfig, IAggregatorConfig } from "./schema"
// import { AggregatorConfig } from "./schema"
interface OracleDeployInfo {
pubkey: PublicKey
owner: PublicKey
}
interface AggregatorDeployInfo {
pubkey: PublicKey
config: IAggregatorConfig
oracles: {
[key: string]: OracleDeployInfo
}
}
export interface AggregatorDeployFile {
programID: PublicKey
aggregators: {
[key: string]: AggregatorDeployInfo
}
}
const FLUX_AGGREGATOR_SO = path.resolve(
__dirname,
"../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 {
// file backed json state
public setup: AggregatorSetupFile
public state: AggregatorDeployFile
constructor(statePath: string, setupFile: string, private wallet: Wallet) {
this.state = stateFromJSON(
statePath,
{
aggregators: {},
} as any,
{
replacer: jsonReplacer,
reviver: jsonReviver,
}
)
this.setup = loadAggregatorSetup(setupFile)
}
async runAll() {
await this.deployProgram()
await this.createAggregators()
}
async deployProgram() {
if (this.state.programID) {
console.log("program deployed")
return
}
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
console.log(`deploying ${FLUX_AGGREGATOR_SO}...`)
const bpfLoader = new BPFLoader(this.wallet)
const account = await bpfLoader.load(programBinary)
this.state.programID = account.publicKey
}
async createAggregators() {
for (let key of Object.keys(this.setup.aggregators)) {
const aggregatorSetup = this.setup.aggregators[key]
let info = this.state.aggregators[key]
if (!info) {
this.state.aggregators[key] = await this.createAggregator(
key,
aggregatorSetup
)
info = this.state.aggregators[key]
}
console.log(`${key} aggregator deployed`)
for (let oracleName of aggregatorSetup.oracles || []) {
const oracleSetup = this.setup.oracles[oracleName]
// TODO: check that key exists
let oinfo = info.oracles[oracleName]
if (!oinfo) {
oinfo = await this.createOracle(info, oracleName, oracleSetup)
// hmm... not triggering save
info.oracles[oracleName] = oinfo
}
console.log(`${key} added oracle:`, oracleSetup.owner)
}
}
}
get program() {
return new FluxAggregator(this.wallet, this.state.programID)
}
async createOracle(
aggregatorInfo: AggregatorDeployInfo,
name: string,
setup: OracleConfig
): Promise<OracleDeployInfo> {
const config = {
description: name,
aggregator: aggregatorInfo.pubkey,
aggregatorOwner: this.wallet.account,
oracleOwner: new PublicKey(setup.owner),
}
const account = await this.program.addOracle(config)
return {
pubkey: account.publicKey,
owner: config.oracleOwner,
}
}
async createAggregator(
name: string,
cfg: AggregatorSetupConfig
): Promise<AggregatorDeployInfo> {
const config = {
description: name,
decimals: cfg.decimals,
minSubmissions: cfg.minSubmissions,
maxSubmissions: cfg.maxSubmissions,
restartDelay: cfg.restartDelay,
rewardTokenAccount: new PublicKey(cfg.rewardTokenAccount || 0),
rewardAmount: cfg.rewardAmount,
}
const account = await this.program.initialize({
// FIXME: move this into initialize method
config: new AggregatorConfig(config),
owner: this.wallet.account,
})
return {
pubkey: account.publicKey,
config,
oracles: {},
}
}
}

View File

@ -104,7 +104,19 @@ class Submission {
} }
} }
export class AggregatorConfig extends Serialization { export interface IAggregatorConfig {
decimals: number
description: string
restartDelay: number
rewardAmount: number
maxSubmissions: number
minSubmissions: number
rewardTokenAccount: PublicKey
}
export class AggregatorConfig
extends Serialization
implements IAggregatorConfig {
public decimals!: number public decimals!: number
public description!: string public description!: string
public restartDelay!: number public restartDelay!: number
@ -248,7 +260,6 @@ export class AddOracle extends InstructionSerialization {
} }
} }
export class RemoveOracle extends InstructionSerialization { export class RemoveOracle extends InstructionSerialization {
public static schema = { public static schema = {
kind: "struct", kind: "struct",
@ -256,6 +267,13 @@ export class RemoveOracle extends InstructionSerialization {
} }
} }
export class Withdraw extends InstructionSerialization {
public static schema = {
kind: "struct",
fields: [["faucetOwnerSeed", ["u8"]]],
}
}
export class Submit extends InstructionSerialization { export class Submit extends InstructionSerialization {
public static schema = { public static schema = {
kind: "struct", kind: "struct",

60
src/state.ts Normal file
View File

@ -0,0 +1,60 @@
import fs from "fs"
// stateFromJSON returns a JSON file backed object. When object mutates, will
// serialize the object as JSON to the specified path.
//
// with recursive proxy support similar to:
//
// https://stackoverflow.com/questions/41299642/how-to-use-javascript-proxy-for-nested-objects
export function stateFromJSON<T extends Object>(
filepath: string,
defaultObject: T,
opts: {
replacer?: Function
reviver?: Function
} = {}
): T {
let root = defaultObject
try {
const data = fs.readFileSync(filepath, "utf8")
// TODO: support custom revive
root = JSON.parse(data, opts.reviver as any)
console.log(`Loaded state object: ${filepath}`)
} catch (err) {
console.log(`Init state object: ${filepath}`)
}
function save() {
// should be sync to avoid write races
fs.writeFileSync(filepath, JSON.stringify(root, opts.replacer as any, 2))
}
const proxy = {
get(target, key) {
if (typeof target[key] === "object" && target[key] !== null) {
return new Proxy(target[key], proxy)
} else {
return target[key]
}
},
set(obj, prop, val) {
obj[prop] = val
save()
return true
},
}
return new Proxy(root, proxy) as T
}
// import { BigNumber } from "ethers";
// function jsonRevive(_key: string, val: any) {
// if (val && typeof val == "object") {
// if (val["type"] == "BigNumber") {
// return BigNumber.from(val["hex"]);
// }
// }
// return val;
// }

207
test.ts
View File

@ -3,11 +3,11 @@ dotenv.config()
import BN from "bn.js" import BN from "bn.js"
import { BPFLoader, ProgramAccount, SPLToken, Wallet } from "solray" 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 fs from "fs"
import path from "path"
import { AggregatorConfig } from "./src/schema" import { AggregatorConfig } from "./src/schema"
import FluxAggregator from "./src/FluxAggregator" import FluxAggregator from "./src/FluxAggregator"
@ -15,138 +15,121 @@ import * as encoding from "./src/schema"
import { Account, AccountInfo, Connection, PublicKey } from "@solana/web3.js" import { Account, AccountInfo, Connection, PublicKey } from "@solana/web3.js"
import { coinbase } from "./src/PriceFeed" import { coinbase } from "./src/PriceFeed"
import { Submitter } from "./src/Submitter" import { Submitter } from "./src/Submitter"
import { Deployer } from "./src/deploy"
const FLUX_AGGREGATOR_SO = path.resolve(__dirname, "build/flux_aggregator.so") import { loadAggregatorSetup } from "./src/config"
import { stateFromJSON } from "./src/state"
async function main() { async function main() {
let ctx = new AppContext() let ctx = new AppContext()
let deployer = await ctx.deployer()
let adminWallet = await ctx.adminWallet() let adminWallet = await ctx.adminWallet()
let oracleWallet = await ctx.oracleWallet() const deployer = new Deployer(
`deploy2.${network}.json`,
console.log(network) `config/setup.${network}.json`,
adminWallet
await conn.requestAirdrop(adminWallet.pubkey, 10 * 1e9)
console.log((await conn.getBalance(adminWallet.pubkey)) / 1e9)
let aggregatorProgram = await deployer.ensure(
"aggregatorProgram",
async () => {
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
console.log(`deploying ${FLUX_AGGREGATOR_SO}...`)
const bpfLoader = new BPFLoader(adminWallet)
return bpfLoader.load(programBinary)
}
) )
const spltoken = new SPLToken(adminWallet) await deployer.runAll()
const rewardToken = await deployer.ensure("create reward token", async () => { console.log("done")
return spltoken.initializeMint({
mintAuthority: adminWallet.pubkey,
decimals: 8,
})
})
const rewardTokenOwner = await ProgramAccount.forSeed( return
Buffer.from("solink"),
aggregatorProgram.publicKey
)
const rewardTokenAccount = await deployer.ensure( // let deployer = await ctx.deployer()
"initialize reward token account",
async () => {
const vault = await spltoken.initializeAccount({
token: rewardToken.publicKey,
owner: rewardTokenOwner.pubkey,
})
await spltoken.mintTo({ // let oracleWallet = await ctx.oracleWallet()
token: rewardToken.publicKey,
to: vault.publicKey,
amount: BigInt(1e6 * 1e8), // 1M
authority: adminWallet.pubkey,
})
return vault // console.log(network)
}
)
console.log(await spltoken.mintInfo(rewardToken.publicKey)) // await conn.requestAirdrop(adminWallet.pubkey, 10 * 1e9)
// console.log((await conn.getBalance(adminWallet.pubkey)) / 1e9)
const program = new FluxAggregator(adminWallet, aggregatorProgram.publicKey) // const spltoken = new SPLToken(adminWallet)
// const rewardToken = await deployer.ensure("create reward token", async () => {
// return spltoken.initializeMint({
// mintAuthority: adminWallet.pubkey,
// decimals: 8,
// })
// })
let aggregator = await deployer.ensure( // const rewardTokenOwner = await ProgramAccount.forSeed(
"create btc:usd aggregator", // Buffer.from("solink"),
async () => { // aggregatorProgram.publicKey
let name = "btc:usd" // )
return program.initialize({
config: new AggregatorConfig({
description: name,
decimals: 2,
minSubmissions: 1,
maxSubmissions: 3,
restartDelay: 0,
rewardAmount: BigInt(10),
rewardTokenAccount: rewardTokenAccount.publicKey,
}),
owner: adminWallet.account,
})
}
)
const N_ORACLES = 4 // const rewardTokenAccount = await deployer.ensure(
interface OracleRole { // "initialize reward token account",
owner: Account // async () => {
oracle: PublicKey // const vault = await spltoken.initializeAccount({
} // token: rewardToken.publicKey,
// owner: rewardTokenOwner.pubkey,
// })
const oracleRoles: OracleRole[] = [] // await spltoken.mintTo({
// token: rewardToken.publicKey,
// to: vault.publicKey,
// amount: BigInt(1e6 * 1e8), // 1M
// authority: adminWallet.pubkey,
// })
for (let i = 0; i < N_ORACLES; i++) { // return vault
// 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( // console.log(await spltoken.mintInfo(rewardToken.publicKey))
`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 N_ORACLES = 4
const submitter = new Submitter( // interface OracleRole {
aggregatorProgram.publicKey, // owner: Account
aggregator.publicKey, // oracle: PublicKey
role.oracle, // }
owner,
priceFeed,
{
// don't submit value unless btc changes at least a dollar
minValueChangeForNewRound: 100,
}
)
submitter.start() // 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))