2021-02-24 18:41:36 -08:00
|
|
|
import { AccountInfo, Connection, EpochInfo } from "@solana/web3.js"
|
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"
|
2021-02-24 18:41:36 -08:00
|
|
|
import { 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 {
|
2021-02-17 23:14:13 -08:00
|
|
|
// 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-24 18:41:36 -08:00
|
|
|
private epoch?: EpochInfo
|
|
|
|
private refreshAccounts: () => Promise<void> = async () => { }
|
2021-02-17 05:39:03 -08:00
|
|
|
|
2021-02-20 03:28:39 -08:00
|
|
|
public reportedRound: BN
|
|
|
|
|
2021-02-17 05:39:03 -08:00
|
|
|
constructor(
|
|
|
|
programID: PublicKey,
|
|
|
|
public aggregatorPK: PublicKey,
|
|
|
|
public oraclePK: PublicKey,
|
|
|
|
private oracleOwnerWallet: Wallet,
|
2021-02-17 23:14:13 -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)
|
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-20 03:28:39 -08:00
|
|
|
public async withdrawRewards() {}
|
|
|
|
|
2021-02-24 18:41:36 -08:00
|
|
|
private async observeAggregatorState() {
|
|
|
|
// load state
|
|
|
|
if (!this.aggregator) {
|
2021-02-20 03:28:39 -08:00
|
|
|
this.aggregator = await Aggregator.load(this.aggregatorPK)
|
|
|
|
}
|
2021-02-24 18:41:36 -08:00
|
|
|
|
|
|
|
let registeredHooks = false
|
|
|
|
const keysToQuery: { [keys: string]: (key: string, acc: AccountInfo<Buffer>) => void } = {
|
|
|
|
[this.oraclePK.toBase58()]: (key, acc) => {
|
|
|
|
this.oracle = Oracle.deserialize(acc.data)
|
|
|
|
log.debug(`Update oracle: ${key}`)
|
|
|
|
},
|
|
|
|
[this.aggregator.answerSubmissions.toBase58()]: (key, acc) => {
|
|
|
|
this.answerSubmissions = Submissions.deserialize(acc.data);
|
|
|
|
log.debug(`Update answerSubmissions: ${key}`)
|
|
|
|
},
|
|
|
|
[this.aggregator.roundSubmissions.toBase58()]: (key, acc) => {
|
|
|
|
this.roundSubmissions = Submissions.deserialize(acc.data);
|
|
|
|
log.debug(`Update roundSubmissions: ${key}`)
|
|
|
|
},
|
|
|
|
}
|
2021-02-20 03:28:39 -08:00
|
|
|
|
2021-02-24 18:41:36 -08:00
|
|
|
this.refreshAccounts = async () => {
|
|
|
|
const { keys, array } = await getMultipleAccounts(conn, Object.keys(keysToQuery));
|
|
|
|
keys.forEach((key, i) => {
|
|
|
|
keysToQuery[key](key, array[i])
|
2021-02-19 02:35:30 -08:00
|
|
|
|
2021-02-24 18:41:36 -08:00
|
|
|
if(!registeredHooks) {
|
|
|
|
conn.onAccountChange(new PublicKey(key), async (info) => {
|
|
|
|
keysToQuery[key](key, info)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
registeredHooks = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
await this.refreshAccounts()
|
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
|
|
|
|
2021-02-24 18:41:36 -08:00
|
|
|
const roundID = this.aggregator.round.id
|
|
|
|
if (!roundID.isZero() && roundID.lte(this.reportedRound)) {
|
|
|
|
this.logger.debug("don't report to the same round twice")
|
|
|
|
return
|
|
|
|
}
|
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-17 23:14:13 -08:00
|
|
|
|
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(),
|
|
|
|
})
|
2021-02-17 23:14:13 -08:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-02-17 05:39:03 -08:00
|
|
|
await this.trySubmit()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async trySubmit() {
|
|
|
|
// 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(),
|
|
|
|
})
|
|
|
|
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-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 {
|
2021-02-17 23:14:13 -08:00
|
|
|
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
|
|
|
|
const value = this.currentValue
|
|
|
|
if (value.isZero()) {
|
2021-02-20 03:28:39 -08:00
|
|
|
this.logger.warn("current value is zero. skip submit")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!roundID.isZero() && roundID.lte(this.reportedRound)) {
|
|
|
|
this.logger.debug("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 18:41:36 -08:00
|
|
|
this.logger.info("Submit OK");
|
2021-02-17 20:15:50 -08:00
|
|
|
} catch (err) {
|
2021-02-17 23:14:13 -08:00
|
|
|
console.log(err)
|
2021-02-20 02:00:17 -08:00
|
|
|
this.logger.error("Submit error", {
|
2021-02-17 23:14:13 -08:00
|
|
|
err: err.toString(),
|
2021-02-17 20:15:50 -08:00
|
|
|
})
|
|
|
|
}
|
2021-02-20 03:28:39 -08:00
|
|
|
|
|
|
|
|
2021-02-17 05:39:03 -08:00
|
|
|
}
|
|
|
|
}
|