2021-02-02 20:44:17 -08:00
|
|
|
import { PublicKey, Account } from "solray"
|
|
|
|
import BN from "bn.js"
|
|
|
|
import { deserialize, serialize } from "borsh"
|
2021-02-17 05:39:03 -08:00
|
|
|
import { conn } from "./context"
|
2021-02-02 20:44:17 -08:00
|
|
|
|
2021-02-15 00:17:45 -08:00
|
|
|
const MAX_ORACLES = 13
|
2021-02-06 00:13:12 -08:00
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
const boolMapper = {
|
|
|
|
encode: boolToInt,
|
|
|
|
decode: intToBool,
|
|
|
|
}
|
|
|
|
|
|
|
|
const pubkeyMapper = {
|
|
|
|
encode: (key: PublicKey) => {
|
|
|
|
// if (key.constructor == PublicKey) {
|
|
|
|
// // key.
|
|
|
|
// } else {
|
|
|
|
// key
|
|
|
|
// }
|
|
|
|
// TODO: support either account or public key
|
|
|
|
return key.toBuffer()
|
|
|
|
},
|
|
|
|
|
|
|
|
decode: (buf: Uint8Array) => {
|
|
|
|
return new PublicKey(buf)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// support strings that can be contained in at most 32 bytes
|
|
|
|
const str32Mapper = {
|
|
|
|
encode: (str: String) => {
|
2021-02-15 00:17:45 -08:00
|
|
|
str = str.substr(0, 32).padEnd(32)
|
2021-02-02 20:44:17 -08:00
|
|
|
return Buffer.from(str, "utf8").slice(0, 32) // truncate at 32 bytes
|
|
|
|
},
|
|
|
|
|
|
|
|
decode: (bytes: Uint8Array) => {
|
|
|
|
return Buffer.from(bytes).toString("utf8").trim()
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-02-17 05:39:03 -08:00
|
|
|
const u64Date = {
|
|
|
|
encode: (date: Date) => {
|
|
|
|
return new BN(Math.floor(date.getTime() / 1000))
|
|
|
|
},
|
|
|
|
|
|
|
|
decode: (unixtime: BN) => {
|
|
|
|
return new Date(unixtime.toNumber() * 1000)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
export abstract class Serialization {
|
|
|
|
public static async load<T>(
|
|
|
|
this: { new (data: any): T },
|
|
|
|
key: PublicKey
|
|
|
|
): Promise<T> {
|
|
|
|
const info = await conn.getAccountInfo(key, "recent")
|
|
|
|
if (!info) {
|
|
|
|
throw new Error("account does not exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
return deserialize(schema, this, info.data)
|
|
|
|
}
|
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
public static deserialize<T>(this: { new (data: any): T }, data: Buffer): T {
|
|
|
|
return deserialize(schema, this, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
public static serialize<T extends Serialization>(
|
|
|
|
this: { new (data: any): T },
|
|
|
|
data: object
|
|
|
|
): Buffer {
|
|
|
|
return new this(data).serialize()
|
|
|
|
}
|
|
|
|
|
|
|
|
public serialize(): Buffer {
|
2021-02-15 00:17:45 -08:00
|
|
|
let buf = Buffer.from(serialize(schema, this))
|
|
|
|
if (buf.length == 0) {
|
|
|
|
throw new Error("serialized buffer is 0. something wrong with schema")
|
|
|
|
}
|
|
|
|
return buf
|
2021-02-02 20:44:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
constructor(data) {
|
|
|
|
Object.assign(this, data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Submission {
|
2021-02-17 20:15:50 -08:00
|
|
|
public updatedAt!: BN
|
2021-02-02 20:44:17 -08:00
|
|
|
public value!: BN
|
|
|
|
public oracle!: PublicKey
|
|
|
|
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
2021-02-17 20:15:50 -08:00
|
|
|
["updatedAt", "u64"],
|
2021-02-02 20:44:17 -08:00
|
|
|
["value", "u64"],
|
|
|
|
["oracle", [32], pubkeyMapper],
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(data: any) {
|
|
|
|
Object.assign(this, data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-06 00:13:12 -08:00
|
|
|
export class AggregatorConfig extends Serialization {
|
|
|
|
public decimals!: number
|
|
|
|
public description!: string
|
|
|
|
public restartDelay!: number
|
|
|
|
public rewardAmount!: number
|
|
|
|
public maxSubmissions!: number
|
|
|
|
public minSubmissions!: number
|
|
|
|
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
|
|
|
["description", [32], str32Mapper],
|
|
|
|
["decimals", "u8"],
|
|
|
|
["restartDelay", "u8"],
|
|
|
|
["maxSubmissions", "u8"],
|
|
|
|
["minSubmissions", "u8"],
|
|
|
|
["rewardAmount", "u64"],
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Submissions extends Serialization {
|
2021-02-17 05:39:03 -08:00
|
|
|
public isInitialized!: boolean
|
|
|
|
public submissions!: Submission[]
|
|
|
|
|
2021-02-06 00:13:12 -08:00
|
|
|
public static size = 625
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
|
|
|
["isInitialized", "u8", boolMapper],
|
2021-02-15 00:17:45 -08:00
|
|
|
["submissions", [Submission, MAX_ORACLES]],
|
2021-02-06 00:13:12 -08:00
|
|
|
],
|
|
|
|
}
|
2021-02-17 05:39:03 -08:00
|
|
|
|
2021-02-17 20:15:50 -08:00
|
|
|
// if not already submitted, and has empty spot
|
|
|
|
public canSubmit(pk: PublicKey, cfg: AggregatorConfig): boolean {
|
|
|
|
if (this.hadSubmitted(pk)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
let emptyIndex = this.submissions.findIndex((s) => {
|
|
|
|
return s.updatedAt.isZero()
|
|
|
|
})
|
|
|
|
|
|
|
|
return emptyIndex > 0 && emptyIndex < cfg.maxSubmissions
|
|
|
|
}
|
|
|
|
|
2021-02-17 05:39:03 -08:00
|
|
|
public hadSubmitted(pk: PublicKey): boolean {
|
|
|
|
return !!this.submissions.find((s) => {
|
|
|
|
return s.oracle.equals(pk)
|
|
|
|
})
|
|
|
|
}
|
2021-02-06 00:13:12 -08:00
|
|
|
}
|
|
|
|
class Round extends Serialization {
|
2021-02-17 05:39:03 -08:00
|
|
|
public id!: BN
|
|
|
|
public createdAt!: BN
|
|
|
|
public updatedAt!: BN
|
|
|
|
|
2021-02-06 00:13:12 -08:00
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
|
|
|
["id", "u64"],
|
2021-02-17 05:39:03 -08:00
|
|
|
["createdAt", "u64"],
|
|
|
|
["updatedAt", "u64"],
|
2021-02-06 00:13:12 -08:00
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Answer extends Serialization {
|
2021-02-17 05:39:03 -08:00
|
|
|
public round_id!: BN
|
2021-02-17 19:12:50 -08:00
|
|
|
public median!: BN
|
2021-02-17 05:39:03 -08:00
|
|
|
public created_at!: BN
|
|
|
|
public updated_at!: BN
|
|
|
|
|
2021-02-06 00:13:12 -08:00
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
|
|
|
["round_id", "u64"],
|
2021-02-17 19:12:50 -08:00
|
|
|
["median", "u64"],
|
2021-02-06 00:13:12 -08:00
|
|
|
["created_at", "u64"],
|
|
|
|
["updated_at", "u64"],
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
export class Aggregator extends Serialization {
|
2021-02-17 19:12:50 -08:00
|
|
|
public static size = 197
|
2021-02-06 00:13:12 -08:00
|
|
|
|
|
|
|
public config!: AggregatorConfig
|
2021-02-15 00:17:45 -08:00
|
|
|
public roundSubmissions!: PublicKey
|
|
|
|
public answerSubmissions!: PublicKey
|
2021-02-17 05:39:03 -08:00
|
|
|
public answer!: Answer
|
|
|
|
public round!: Round
|
2021-02-02 20:44:17 -08:00
|
|
|
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
2021-02-06 00:13:12 -08:00
|
|
|
["config", AggregatorConfig],
|
|
|
|
["isInitialized", "u8", boolMapper],
|
2021-02-02 20:44:17 -08:00
|
|
|
["owner", [32], pubkeyMapper],
|
2021-02-06 00:13:12 -08:00
|
|
|
["round", Round],
|
2021-02-15 00:17:45 -08:00
|
|
|
["roundSubmissions", [32], pubkeyMapper],
|
2021-02-06 00:13:12 -08:00
|
|
|
["answer", Answer],
|
2021-02-15 00:17:45 -08:00
|
|
|
["answerSubmissions", [32], pubkeyMapper],
|
2021-02-02 20:44:17 -08:00
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class InstructionSerialization extends Serialization {
|
|
|
|
public serialize(): Buffer {
|
|
|
|
return new Instruction({ [this.constructor.name]: this }).serialize()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Initialize extends InstructionSerialization {
|
2021-02-06 00:13:12 -08:00
|
|
|
// public submitInterval!: number
|
|
|
|
// public minSubmissionValue!: number
|
|
|
|
// public maxSubmissionValue!: number
|
|
|
|
// public submissionDecimals!: number
|
|
|
|
// /// A short description of what is being reported
|
|
|
|
// public description!: string
|
2021-02-02 20:44:17 -08:00
|
|
|
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
2021-02-06 00:13:12 -08:00
|
|
|
fields: [["config", AggregatorConfig]],
|
2021-02-02 20:44:17 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-15 00:17:45 -08:00
|
|
|
export class Configure extends InstructionSerialization {
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [["config", AggregatorConfig]],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class AddOracle extends InstructionSerialization {
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [["description", [32], str32Mapper]],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class RemoveOracle extends InstructionSerialization {
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Submit extends InstructionSerialization {
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
|
|
|
["round_id", "u64"],
|
|
|
|
["value", "u64"],
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
export class Instruction extends Serialization {
|
|
|
|
public enum!: string
|
|
|
|
|
|
|
|
public static schema = {
|
|
|
|
kind: "enum",
|
|
|
|
field: "enum",
|
2021-02-15 00:17:45 -08:00
|
|
|
values: [
|
|
|
|
[Initialize.name, Initialize],
|
|
|
|
[Configure.name, Configure],
|
|
|
|
[AddOracle.name, AddOracle],
|
|
|
|
[RemoveOracle.name, RemoveOracle],
|
|
|
|
[Submit.name, Submit],
|
|
|
|
],
|
2021-02-02 20:44:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
public constructor(prop: { [key: string]: any }) {
|
|
|
|
super({})
|
|
|
|
// deserializer calls the construction with `{ [enum]: value }`, so we need
|
|
|
|
// to figure out the enum type
|
|
|
|
//
|
|
|
|
// expect only one key-value (what a retarded interface)
|
|
|
|
for (let key of Object.keys(prop)) {
|
|
|
|
this.enum = key
|
|
|
|
this[key] = prop[key]
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error("not an expected enum object")
|
|
|
|
}
|
|
|
|
|
|
|
|
public get value() {
|
|
|
|
return this[this.enum]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function intToBool(i: number) {
|
|
|
|
if (i == 0) {
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function boolToInt(t: boolean) {
|
|
|
|
if (t) {
|
|
|
|
return 1
|
|
|
|
} else {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-15 00:17:45 -08:00
|
|
|
export class Oracle extends Serialization {
|
2021-02-06 00:13:12 -08:00
|
|
|
public static size = 113
|
2021-02-17 05:39:03 -08:00
|
|
|
public allowStartRound!: BN
|
2021-02-15 00:17:45 -08:00
|
|
|
|
|
|
|
public static schema = {
|
|
|
|
kind: "struct",
|
|
|
|
fields: [
|
|
|
|
["description", [32], str32Mapper],
|
|
|
|
["isInitialized", "u8", boolMapper],
|
|
|
|
["withdrawable", "u64"],
|
2021-02-17 05:39:03 -08:00
|
|
|
["allowStartRound", "u64"],
|
2021-02-15 00:17:45 -08:00
|
|
|
["aggregator", [32], pubkeyMapper],
|
|
|
|
["owner", [32], pubkeyMapper],
|
|
|
|
],
|
|
|
|
}
|
2021-02-17 05:39:03 -08:00
|
|
|
|
|
|
|
public canStartNewRound(round: BN): boolean {
|
|
|
|
return this.allowStartRound.lte(round)
|
|
|
|
}
|
2021-02-06 00:13:12 -08:00
|
|
|
}
|
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
// if there is optional or variable length items, what is: borsh_utils::get_packed_len::<Submission>()?
|
|
|
|
//
|
|
|
|
// would panic given variable sized types
|
|
|
|
|
|
|
|
export const schema = new Map([
|
|
|
|
[Aggregator, Aggregator.schema],
|
2021-02-15 00:17:45 -08:00
|
|
|
[Oracle, Oracle.schema],
|
2021-02-06 00:13:12 -08:00
|
|
|
[Round, Round.schema],
|
|
|
|
[Answer, Answer.schema],
|
|
|
|
[AggregatorConfig, AggregatorConfig.schema],
|
|
|
|
[Submissions, Submissions.schema],
|
2021-02-02 20:44:17 -08:00
|
|
|
[Submission, Submission.schema],
|
2021-02-15 00:17:45 -08:00
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
[Instruction, Instruction.schema],
|
2021-02-15 00:17:45 -08:00
|
|
|
[Initialize, Initialize.schema],
|
|
|
|
[AddOracle, AddOracle.schema],
|
|
|
|
[Submit, Submit.schema],
|
|
|
|
|
2021-02-02 20:44:17 -08:00
|
|
|
] as any) as any
|