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": "",
"license": "ISC",
"dependencies": {
"@ltd/j-toml": "^1.6.0",
"@solana/web3.js": "^0.90.5",
"buffer-layout": "^1.2.0",
"commander": "^6.2.0",

View File

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

View File

@ -28,6 +28,7 @@ interface SubmitterConfig {
export class Submitter {
public aggregator!: Aggregator
public oracle!: Oracle
public roundSubmissions!: Submissions
public answerSubmissions!: Submissions
public program: FluxAggregator
@ -40,7 +41,7 @@ export class Submitter {
public oraclePK: PublicKey,
private oracleOwnerWallet: Wallet,
private priceFeed: IPriceFeed,
private cfg: SubmitterConfig,
private cfg: SubmitterConfig
) {
this.program = new FluxAggregator(this.oracleOwnerWallet, programID)
@ -66,6 +67,10 @@ export class Submitter {
await Promise.all([this.observeAggregatorState(), this.observePriceFeed()])
}
public async withdrawRewards() {
}
private async observeAggregatorState() {
conn.onAccountChange(this.aggregatorPK, async (info) => {
this.aggregator = Aggregator.deserialize(info.data)
@ -96,9 +101,13 @@ export class Submitter {
this.currentValue = new BN(price.value)
const valueDiff = this.aggregator.answer.median.sub(this.currentValue).abs()
if(valueDiff.lten(this.cfg.minValueChangeForNewRound)) {
this.logger.debug("price did not change enough to start a new round", { diff: valueDiff.toNumber()});
const valueDiff = this.aggregator.answer.median
.sub(this.currentValue)
.abs()
if (valueDiff.lten(this.cfg.minValueChangeForNewRound)) {
this.logger.debug("price did not change enough to start a new round", {
diff: valueDiff.toNumber(),
})
continue
}
@ -110,6 +119,9 @@ export class Submitter {
// TODO: make it possible to be triggered by chainlink task
// 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
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 description!: string
public restartDelay!: number
@ -248,7 +260,6 @@ export class AddOracle extends InstructionSerialization {
}
}
export class RemoveOracle extends InstructionSerialization {
public static schema = {
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 {
public static schema = {
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 { BPFLoader, ProgramAccount, SPLToken, Wallet } from "solray"
import { ProgramAccount, SPLToken, Wallet } from "solray"
import { AppContext, conn, network } from "./src/context"
import fs from "fs"
import path from "path"
import { AggregatorConfig } from "./src/schema"
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 { coinbase } from "./src/PriceFeed"
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() {
let ctx = new AppContext()
let deployer = await ctx.deployer()
let adminWallet = await ctx.adminWallet()
let oracleWallet = await ctx.oracleWallet()
console.log(network)
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 deployer = new Deployer(
`deploy2.${network}.json`,
`config/setup.${network}.json`,
adminWallet
)
const spltoken = new SPLToken(adminWallet)
const rewardToken = await deployer.ensure("create reward token", async () => {
return spltoken.initializeMint({
mintAuthority: adminWallet.pubkey,
decimals: 8,
})
})
await deployer.runAll()
console.log("done")
const rewardTokenOwner = await ProgramAccount.forSeed(
Buffer.from("solink"),
aggregatorProgram.publicKey
)
return
const rewardTokenAccount = await deployer.ensure(
"initialize reward token account",
async () => {
const vault = await spltoken.initializeAccount({
token: rewardToken.publicKey,
owner: rewardTokenOwner.pubkey,
})
// let deployer = await ctx.deployer()
await spltoken.mintTo({
token: rewardToken.publicKey,
to: vault.publicKey,
amount: BigInt(1e6 * 1e8), // 1M
authority: adminWallet.pubkey,
})
// let oracleWallet = await ctx.oracleWallet()
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(
"create btc:usd aggregator",
async () => {
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 rewardTokenOwner = await ProgramAccount.forSeed(
// Buffer.from("solink"),
// aggregatorProgram.publicKey
// )
const N_ORACLES = 4
interface OracleRole {
owner: Account
oracle: PublicKey
}
// const rewardTokenAccount = await deployer.ensure(
// "initialize reward token account",
// async () => {
// 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++) {
// TODO: probably put the desired oracles in a config file...
let owner = await deployer.ensure(`create oracle[${i}] owner`, async () => {
return new Account()
})
// return vault
// }
// )
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,
})
}
)
// console.log(await spltoken.mintInfo(rewardToken.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,
}
)
// const N_ORACLES = 4
// interface OracleRole {
// owner: Account
// oracle: PublicKey
// }
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))