import { PublicKey, Account } from "solray" import BN from "bn.js" import { deserialize, serialize } from "borsh" import { conn } from "./context" import { jsonReplacer } from "./json" const MAX_ORACLES = 13 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) => { str = str.substr(0, 32).padEnd(32) return Buffer.from(str, "utf8").slice(0, 32) // truncate at 32 bytes }, decode: (bytes: Uint8Array) => { return Buffer.from(bytes).toString("utf8").trim() }, } 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( this: { new (data: any): T }, key: PublicKey ): Promise { const info = await conn.getAccountInfo(key, "recent") if (!info) { throw new Error("account does not exist") } return deserialize(schema, this, info.data) } public static deserialize(this: { new (data: any): T }, data: Buffer): T { return deserialize(schema, this, data) } public static serialize( this: { new (data: any): T }, data: object ): Buffer { return new this(data).serialize() } public serialize(): Buffer { let buf = Buffer.from(serialize(schema, this)) if (buf.length == 0) { throw new Error("serialized buffer is 0. something wrong with schema") } return buf } // public toJSON(pretty = true) { // return JSON.stringify( // this[Serialization.DATA_KEY], // jsonReplacer, // pretty ? 2 : 0 // ) // } // public static DATA_KEY = Symbol("DATA") constructor(data) { // this[Serialization.DATA_KEY] = data Object.assign(this, data) } } class Submission { public updatedAt!: BN public value!: BN public oracle!: PublicKey public static schema = { kind: "struct", fields: [ ["updatedAt", "u64"], ["value", "u64"], ["oracle", [32], pubkeyMapper], ], } constructor(data: any) { Object.assign(this, data) } } 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 public rewardAmount!: number public maxSubmissions!: number public minSubmissions!: number public rewardTokenAccount!: PublicKey public static schema = { kind: "struct", fields: [ ["description", [32], str32Mapper], ["decimals", "u8"], ["restartDelay", "u8"], ["maxSubmissions", "u8"], ["minSubmissions", "u8"], ["rewardAmount", "u64"], ["rewardTokenAccount", [32], pubkeyMapper], ], } } export class Submissions extends Serialization { public isInitialized!: boolean public submissions!: Submission[] public static size = 625 public static schema = { kind: "struct", fields: [ ["isInitialized", "u8", boolMapper], ["submissions", [Submission, MAX_ORACLES]], ], } // 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 } public hadSubmitted(pk: PublicKey): boolean { return !!this.submissions.find((s) => { return s.oracle.equals(pk) }) } } export class Round extends Serialization { public id!: BN public createdAt!: BN public updatedAt!: BN public static schema = { kind: "struct", fields: [ ["id", "u64"], ["createdAt", "u64"], ["updatedAt", "u64"], ], } } export class Answer extends Serialization { public roundID!: BN public median!: BN public createdAt!: BN public updatedAt!: BN public static schema = { kind: "struct", fields: [ ["roundID", "u64"], ["median", "u64"], ["createdAt", "u64"], ["updatedAt", "u64"], ], } } export class Aggregator extends Serialization { public static size = 229 public config!: AggregatorConfig public roundSubmissions!: PublicKey public answerSubmissions!: PublicKey public answer!: Answer public round!: Round public static schema = { kind: "struct", fields: [ ["config", AggregatorConfig], ["isInitialized", "u8", boolMapper], ["owner", [32], pubkeyMapper], ["round", Round], ["roundSubmissions", [32], pubkeyMapper], ["answer", Answer], ["answerSubmissions", [32], pubkeyMapper], ], } } abstract class InstructionSerialization extends Serialization { public serialize(): Buffer { return new Instruction({ [this.constructor.name]: this }).serialize() } } export class Initialize extends InstructionSerialization { // public submitInterval!: number // public minSubmissionValue!: number // public maxSubmissionValue!: number // public submissionDecimals!: number // /// A short description of what is being reported // public description!: string public static schema = { kind: "struct", fields: [["config", AggregatorConfig]], } } 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 Withdraw extends InstructionSerialization { public static schema = { kind: "struct", fields: [["faucetOwnerSeed", ["u8"]]], } } export class Submit extends InstructionSerialization { public static schema = { kind: "struct", fields: [ ["round_id", "u64"], ["value", "u64"], ], } } export class Instruction extends Serialization { public enum!: string public static schema = { kind: "enum", field: "enum", values: [ [Initialize.name, Initialize], [Configure.name, Configure], [AddOracle.name, AddOracle], [RemoveOracle.name, RemoveOracle], [Submit.name, Submit], ], } 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 } } export class Oracle extends Serialization { public static size = 113 public allowStartRound!: BN public withdrawable!: BN public static schema = { kind: "struct", fields: [ ["description", [32], str32Mapper], ["isInitialized", "u8", boolMapper], ["withdrawable", "u64"], ["allowStartRound", "u64"], ["aggregator", [32], pubkeyMapper], ["owner", [32], pubkeyMapper], ], } public canStartNewRound(round: BN): boolean { return this.allowStartRound.lte(round) } } // if there is optional or variable length items, what is: borsh_utils::get_packed_len::()? // // would panic given variable sized types export const schema = new Map([ [Aggregator, Aggregator.schema], [Oracle, Oracle.schema], [Round, Round.schema], [Answer, Answer.schema], [AggregatorConfig, AggregatorConfig.schema], [Submissions, Submissions.schema], [Submission, Submission.schema], [Instruction, Instruction.schema], [Initialize, Initialize.schema], [AddOracle, AddOracle.schema], [Submit, Submit.schema], ] as any) as any