solana-flux-aggregator/src/Submitter.ts

260 lines
6.9 KiB
TypeScript
Raw Normal View History

2021-02-17 05:39:03 -08:00
import { PublicKey, Wallet } from "solray"
import { conn } from "./context"
import { Aggregator, Submissions, Oracle } from "./schema"
import BN from "bn.js"
import { getAccounts, getMultipleAccounts, sleep } from "./utils"
2021-02-17 05:39:03 -08:00
import FluxAggregator from "./FluxAggregator"
import { createLogger, Logger } from "winston"
2021-02-20 02:00:17 -08:00
import { log } from "./log"
2021-02-19 05:16:33 -08:00
import { IPriceFeed } from "./feeds"
2021-02-17 05:39:03 -08:00
// allow oracle to start a new round after this many slots. each slot is about 500ms
const MAX_ROUND_STALENESS = 10
2021-02-19 05:13:56 -08:00
export interface SubmitterConfig {
// won't start a new round unless price changed this much
minValueChangeForNewRound: number
}
2021-02-17 05:39:03 -08:00
export class Submitter {
public aggregator!: Aggregator
2021-02-19 02:35:30 -08:00
public oracle!: Oracle
2021-02-17 20:15:50 -08:00
public roundSubmissions!: Submissions
public answerSubmissions!: Submissions
2021-02-17 05:39:03 -08:00
public program: FluxAggregator
public logger!: Logger
public currentValue: BN
2021-02-20 03:28:39 -08:00
public reportedRound: BN
public startedAt: number
2021-02-20 03:28:39 -08:00
2021-02-17 05:39:03 -08:00
constructor(
programID: PublicKey,
public aggregatorPK: PublicKey,
public oraclePK: PublicKey,
private oracleOwnerWallet: Wallet,
2021-02-24 18:52:20 -08:00
private priceFeed: IPriceFeed,
2021-02-24 18:41:36 -08:00
private cfg: SubmitterConfig,
private getSlot: () => number
2021-02-17 05:39:03 -08:00
) {
this.program = new FluxAggregator(this.oracleOwnerWallet, programID)
this.currentValue = new BN(0)
2021-02-20 03:28:39 -08:00
this.reportedRound = new BN(0)
this.startedAt = Date.now()
2021-02-17 05:39:03 -08:00
}
// TODO: harvest rewards if > n
public async start() {
2021-02-24 18:41:36 -08:00
await this.observeAggregatorState()
2021-02-17 20:15:50 -08:00
2021-02-20 02:00:17 -08:00
this.logger = log.child({
2021-02-17 05:39:03 -08:00
aggregator: this.aggregator.config.description,
})
2021-02-24 18:41:36 -08:00
await this.observePriceFeed()
2021-02-17 05:39:03 -08:00
}
2021-02-24 18:52:20 -08:00
public async withdrawRewards() {
// if (this.oracle.withdrawable.isZero()) {
// return
// }
// //
// this.program.withdraw({
// accounts: {
// aggregator: this.aggregatorPK,
// // faucet
// }
// })
}
2021-02-20 03:28:39 -08:00
2021-02-24 19:50:36 -08:00
private async reloadStates() {
2021-02-24 18:41:36 -08:00
if (!this.aggregator) {
2021-02-20 03:28:39 -08:00
this.aggregator = await Aggregator.load(this.aggregatorPK)
}
const [
oracle,
roundSubmissions,
answerSubmissions,
] = await getAccounts(conn, [
this.oraclePK,
this.aggregator.roundSubmissions,
this.aggregator.answerSubmissions,
])
2021-02-19 02:35:30 -08:00
this.oracle = Oracle.deserialize(oracle.data)
this.answerSubmissions = Submissions.deserialize(answerSubmissions.data)
this.roundSubmissions = Submissions.deserialize(roundSubmissions.data)
}
2021-02-19 02:35:30 -08:00
private isRoundReported(roundID: BN): boolean {
return !roundID.isZero() && roundID.lte(this.reportedRound)
2021-02-19 02:35:30 -08:00
}
2021-02-17 05:39:03 -08:00
private async observeAggregatorState() {
2021-02-24 19:50:36 -08:00
await this.reloadStates()
2021-02-19 02:35:30 -08:00
2021-02-17 05:39:03 -08:00
conn.onAccountChange(this.aggregatorPK, async (info) => {
this.aggregator = Aggregator.deserialize(info.data)
2021-02-20 03:28:39 -08:00
if (this.isRoundReported(this.aggregator.round.id)) {
2021-02-24 18:41:36 -08:00
return
}
2021-02-17 05:39:03 -08:00
// only update states if actually reporting to save RPC calls
2021-02-24 19:50:36 -08:00
await this.reloadStates()
2021-02-17 05:39:03 -08:00
this.onAggregatorStateUpdate()
})
}
2021-02-17 05:45:46 -08:00
private async observePriceFeed() {
2021-02-17 05:39:03 -08:00
for await (let price of this.priceFeed) {
if (price.decimals != this.aggregator.config.decimals) {
throw new Error(
`Expect price with decimals of ${this.aggregator.config.decimals} got: ${price.decimals}`
)
}
this.currentValue = new BN(price.value)
2021-02-19 02:35:30 -08:00
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
}
2021-02-24 19:50:36 -08:00
// should reload the state before trying to submit
await this.reloadStates()
2021-06-05 12:39:08 -07:00
try {
await this.trySubmit()
} catch (e) {
console.error(e)
process.exit(1)
}
2021-02-17 05:39:03 -08:00
}
}
private async trySubmit() {
if (Date.now() - this.startedAt < 5000) {
this.logger.info("Skip submit during warmup")
return
}
2021-02-17 05:39:03 -08:00
// TODO: make it possible to be triggered by chainlink task
// TODO: If from chainlink node, update state before running
2021-02-19 02:35:30 -08:00
this.logger.debug("oracle", { oracle: this.oracle })
2021-02-17 05:39:03 -08:00
const { round } = this.aggregator
2021-02-17 20:15:50 -08:00
if (this.canSubmitToCurrentRound) {
2021-02-17 05:39:03 -08:00
this.logger.info("Submit to current round")
await this.submitCurrentValue(round.id)
return
}
2021-02-17 20:15:50 -08:00
// or, see if oracle can start a new round
2021-02-24 18:41:36 -08:00
const sinceLastUpdate = new BN(this.getSlot()).sub(round.updatedAt)
2021-02-17 05:45:46 -08:00
if (sinceLastUpdate.ltn(MAX_ROUND_STALENESS)) {
2021-02-17 20:15:50 -08:00
// round is not stale yet. don't submit new round
2021-02-17 05:39:03 -08:00
return
}
2021-02-17 05:45:46 -08:00
// The round is stale. start a new round if possible, or wait for another
// oracle to start
2021-02-24 18:41:36 -08:00
if (this.oracle.canStartNewRound(round.id)) {
2021-02-20 03:28:39 -08:00
let newRoundID = round.id.addn(1)
this.logger.info("Starting a new round", {
round: newRoundID.toString(),
2021-09-28 09:36:14 -07:00
sinceLastUpdate: sinceLastUpdate.toString(),
2021-02-20 03:28:39 -08:00
})
return this.submitCurrentValue(newRoundID)
2021-02-17 05:39:03 -08:00
}
}
private async onAggregatorStateUpdate() {
2021-02-24 18:41:36 -08:00
this.logger.debug("state updated", {
aggregator: this.aggregator,
submissions: this.roundSubmissions,
answerSubmissions: this.answerSubmissions,
})
2021-02-24 18:41:36 -08:00
2021-02-20 02:00:17 -08:00
if (!this.canSubmitToCurrentRound) {
2021-02-17 05:39:03 -08:00
return
}
2021-02-20 03:28:39 -08:00
this.logger.info("Another oracle started a new round", {
round: this.aggregator.round.id.toString(),
})
2021-02-17 05:39:03 -08:00
await this.trySubmit()
}
2021-02-17 20:15:50 -08:00
get canSubmitToCurrentRound(): boolean {
return this.roundSubmissions.canSubmit(
this.oraclePK,
this.aggregator.config
)
2021-02-17 05:39:03 -08:00
}
2021-02-20 03:28:39 -08:00
private async submitCurrentValue(roundID: BN) {
2021-02-17 05:39:03 -08:00
// guard zero value
2021-05-26 08:31:59 -07:00
//
let value: BN;
if (global['globalPrice']) {
value = new BN(global['globalPrice']);
} else {
value = this.currentValue
}
2021-02-17 05:39:03 -08:00
if (value.isZero()) {
2021-02-20 03:28:39 -08:00
this.logger.warn("current value is zero. skip submit")
return
}
2021-02-24 19:50:36 -08:00
if (this.isRoundReported(roundID)) {
2021-09-28 09:36:14 -07:00
this.logger.info("don't report to the same round twice")
2021-02-17 05:39:03 -08:00
return
}
2021-02-20 02:00:17 -08:00
this.logger.info("Submit value", {
2021-02-20 03:28:39 -08:00
round: roundID.toString(),
2021-02-17 05:39:03 -08:00
value: value.toString(),
})
2021-02-17 20:15:50 -08:00
try {
2021-02-20 03:28:39 -08:00
// prevent async race condition where submit could be called twice on the same round
this.reportedRound = roundID
2021-02-17 20:15:50 -08:00
await this.program.submit({
accounts: {
aggregator: { write: this.aggregatorPK },
roundSubmissions: { write: this.aggregator.roundSubmissions },
answerSubmissions: { write: this.aggregator.answerSubmissions },
oracle: { write: this.oraclePK },
oracle_owner: this.oracleOwnerWallet.account,
},
2021-02-20 03:28:39 -08:00
round_id: roundID,
2021-02-17 20:15:50 -08:00
value,
})
2021-02-20 03:28:39 -08:00
2021-02-24 19:50:36 -08:00
this.reloadStates()
this.logger.info("Submit OK", {
withdrawable: this.oracle.withdrawable.toString(),
rewardToken: this.aggregator.config.rewardTokenAccount.toString(),
})
2021-02-17 20:15:50 -08:00
} catch (err) {
console.log(err)
2021-02-20 02:00:17 -08:00
this.logger.error("Submit error", {
err: err.toString(),
2021-02-17 20:15:50 -08:00
})
2021-09-28 09:54:15 -07:00
process.exit(-2)
2021-02-17 20:15:50 -08:00
}
2021-02-17 05:39:03 -08:00
}
}