This commit is contained in:
De Facto 2021-02-05 21:40:09 +08:00
parent 01a910e7aa
commit 7a5112147f
11 changed files with 444 additions and 258 deletions

View File

@ -51,11 +51,7 @@ Create the `btc:usd` feed (that accepts max and min u64 as valid submission valu
``` ```
yarn solink add-aggregator \ yarn solink add-aggregator \
--feedName btc:usd \ --feedName btc:usd
--submitInterval 6 \
--minSubmissionValue 0 \
--maxSubmissionValue 18446744073709551615 \
--submissionDecimals 2
feed initialized, pubkey: 3aTBom2uodyWkuVPiUkwCZ2HiFywdUx9tp7su7U2H4Nx feed initialized, pubkey: 3aTBom2uodyWkuVPiUkwCZ2HiFywdUx9tp7su7U2H4Nx
``` ```

View File

@ -13,7 +13,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@solana/web3.js": "^0.87.1", "@solana/web3.js": "^0.90.5",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"commander": "^6.2.0", "commander": "^6.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View File

@ -16,10 +16,10 @@ thiserror = "1.0"
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
num_enum = "0.5.1" num_enum = "0.5.1"
hex = "0.4"
[dev-dependencies] [dev-dependencies]
solana-sdk = "1.4.8" solana-sdk = "1.4.8"
hex = "0.4"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -1,11 +1,14 @@
//! Program entrypoint //! Program entrypoint
use crate::{processor::Processor}; use crate::processor::Processor;
use solana_program::{ use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
msg,
}; };
use hex;
entrypoint!(process_instruction); entrypoint!(process_instruction);
// Program entrypoint's implementation // Program entrypoint's implementation
@ -14,11 +17,12 @@ fn process_instruction<'a>(
accounts: &'a [AccountInfo<'a>], accounts: &'a [AccountInfo<'a>],
instruction_data: &[u8], instruction_data: &[u8],
) -> ProgramResult { ) -> ProgramResult {
if let Err(error) = Processor::process(program_id, accounts, instruction_data) { msg!("instruction: {}", hex::encode(instruction_data));
// catch the error so we can print it // if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
// error.print::<Error>(); // // catch the error so we can print it
// msg!("{:?}", error); // // error.print::<Error>();
return Err(error); // // msg!("{:?}", error);
} // return Err(error);
// }
Ok(()) Ok(())
} }

View File

@ -38,3 +38,23 @@ pub enum Instruction {
faucet_owner_seed: [u8; 32], faucet_owner_seed: [u8; 32],
}, },
} }
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use solana_program::{entrypoint::ProgramResult, program_error::ProgramError};
use super::*;
#[test]
fn test_decode_instruction() -> ProgramResult {
let input = hex::decode("004254433a5553442020202020202020202020202020202020202020202020202002010c010100000000000000").map_err(|_| ProgramError::InvalidInstructionData)?;
let inx = Instruction::try_from_slice(&input)
.map_err(|_| ProgramError::InvalidInstructionData)?;
println!("{:?}", inx);
Ok(())
}
}

View File

@ -42,8 +42,8 @@ impl<'a, 'b> Accounts<'a, 'b> {
struct InitializeContext<'a> { struct InitializeContext<'a> {
rent: Rent, rent: Rent,
aggregator: &'a AccountInfo<'a>, aggregator: &'a AccountInfo<'a>,
aggregator_owner: &'a AccountInfo<'a>, aggregator_owner: &'a AccountInfo<'a>, // signed
round_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator round_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
answer_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator answer_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
config: AggregatorConfig, config: AggregatorConfig,

View File

@ -187,3 +187,24 @@ impl IsInitialized for Oracle {
} }
} }
impl InitBorshState for Oracle {} impl InitBorshState for Oracle {}
mod tests {
use crate::borsh_utils;
use super::*;
#[test]
fn test_packed_len() {
println!(
"Aggregator len: {}",
borsh_utils::get_packed_len::<Aggregator>()
);
println!(
"Submissions len: {}",
borsh_utils::get_packed_len::<Submissions>()
);
println!("Oracle len: {}", borsh_utils::get_packed_len::<Oracle>());
}
}

View File

@ -1,18 +1,27 @@
import { import {
PublicKey, BaseProgram, Account, PublicKey,
Wallet, System, SPLToken BaseProgram,
} from "solray"; Account,
Wallet,
System,
SPLToken,
} from "solray"
import { import {
SYSVAR_RENT_PUBKEY, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY,
TransactionInstruction, SystemProgram SYSVAR_CLOCK_PUBKEY,
} from "@solana/web3.js"; TransactionInstruction,
SystemProgram,
import { publicKey, u64LEBuffer, uint64, BufferLayout } from "solray/lib/util/encoding"; } from "@solana/web3.js"
import { import {
decodeOracleInfo publicKey,
} from "./utils" u64LEBuffer,
uint64,
BufferLayout,
} from "solray/lib/util/encoding"
import { decodeOracleInfo } from "./utils"
// @ts-ignore // @ts-ignore
// import BufferLayout from "buffer-layout"; // import BufferLayout from "buffer-layout";
@ -21,125 +30,147 @@ import { schema } from "./schema"
import * as encoding from "./schema" import * as encoding from "./schema"
import { deserialize, serialize } from "borsh" import { deserialize, serialize } from "borsh"
export const AggregatorLayout = BufferLayout.struct([ export const AggregatorLayout = BufferLayout.struct([])
BufferLayout.blob(4, "submitInterval"),
uint64("minSubmissionValue"),
uint64("maxSubmissionValue"),
BufferLayout.u8("submissionDecimals"),
BufferLayout.blob(32, "description"),
BufferLayout.u8("isInitialized"),
publicKey('owner'),
BufferLayout.blob(576, "submissions"),
]);
export const OracleLayout = BufferLayout.struct([ export const OracleLayout = BufferLayout.struct([
uint64("nextSubmitTime"), uint64("nextSubmitTime"),
BufferLayout.blob(32, "description"), BufferLayout.blob(32, "description"),
BufferLayout.u8("isInitialized"), BufferLayout.u8("isInitialized"),
uint64("withdrawable"), uint64("withdrawable"),
publicKey('aggregator'), publicKey("aggregator"),
publicKey('owner'), publicKey("owner"),
]); ])
export const SubmissionLayout = BufferLayout.struct([ export const SubmissionLayout = BufferLayout.struct([
uint64("time"), uint64("time"),
uint64("value"), uint64("value"),
publicKey('oracle'), publicKey("oracle"),
]); ])
interface InitializeParams { interface InitializeParams {
submitInterval: number; config: encoding.AggregatorConfig
minSubmissionValue: bigint; owner: Account
maxSubmissionValue: bigint;
submissionDecimals: number;
description: string;
owner: Account;
} }
interface InitializeInstructionParams extends InitializeParams { interface InitializeInstructionParams extends InitializeParams {
aggregator: PublicKey; aggregator: PublicKey
} }
interface AddOracleParams { interface AddOracleParams {
owner: PublicKey; owner: PublicKey
description: string; description: string
aggregator: PublicKey; aggregator: PublicKey
// To prove you are the aggregator owner // To prove you are the aggregator owner
aggregatorOwner: Account; aggregatorOwner: Account
} }
interface AddOracleInstructionParams extends AddOracleParams { interface AddOracleInstructionParams extends AddOracleParams {
oracle: PublicKey; oracle: PublicKey
} }
interface RemoveOracleParams { interface RemoveOracleParams {
aggregator: PublicKey; aggregator: PublicKey
oracle: PublicKey; oracle: PublicKey
// To prove you are the aggregator owner // To prove you are the aggregator owner
authority?: Account; authority?: Account
} }
interface RemoveOracleInstructionParams extends RemoveOracleParams { interface RemoveOracleInstructionParams extends RemoveOracleParams {}
}
interface SubmitParams { interface SubmitParams {
aggregator: PublicKey; aggregator: PublicKey
oracle: PublicKey; oracle: PublicKey
// The oracle"s index // The oracle"s index
submission: bigint; submission: bigint
// oracle owner // oracle owner
owner: Account; owner: Account
} }
interface SubmitInstructionParams extends SubmitParams { interface SubmitInstructionParams extends SubmitParams {}
}
interface WithdrawParams { interface WithdrawParams {
aggregator: PublicKey, aggregator: PublicKey
// withdraw to // withdraw to
receiver: PublicKey, receiver: PublicKey
// withdraw amount // withdraw amount
amount: bigint, amount: bigint
tokenAccount: PublicKey, tokenAccount: PublicKey
tokenOwner: PublicKey, tokenOwner: PublicKey
// signer // signer
authority: Account, authority: Account
} }
interface WithdrawInstructionParams extends WithdrawParams { interface WithdrawInstructionParams extends WithdrawParams {}
}
export default class FluxAggregator extends BaseProgram { export default class FluxAggregator extends BaseProgram {
private sys: System private sys: System
constructor(wallet: Wallet, programID: PublicKey) { constructor(wallet: Wallet, programID: PublicKey) {
super(wallet, programID); super(wallet, programID)
this.sys = new System(this.wallet); this.sys = new System(this.wallet)
} }
public async initialize(params: InitializeParams): Promise<Account> { public async initialize(params: InitializeParams): Promise<Account> {
const account = new Account(); const aggregator = new Account()
const answer_submissions = new Account()
const round_submissions = new Account()
await this.sendTx([ console.log({ config: params.config, program: this.programID.toString() })
await this.sys.createRentFreeAccountInstruction({
newPubicKey: account.publicKey,
space: AggregatorLayout.span,
programID: this.programID,
}),
this.initializeInstruction({
...params,
aggregator: account.publicKey,
})
], [this.account, account, params.owner]);
return account; const input = encoding.Initialize.serialize({ config: params.config })
// console.log("init instruction", input.toString("hex"))
console.log("init instruction", input.toString("hex"))
console.log("rent", [encoding.Aggregator.size, encoding.Submissions.size])
console.log("wallet", this.account.publicKey.toString())
await this.sendTx(
[
// await this.sys.createRentFreeAccountInstruction({
// newPubicKey: aggregator.publicKey,
// space: encoding.Aggregator.size,
// programID: this.programID,
// }),
// await this.sys.createRentFreeAccountInstruction({
// newPubicKey: answer_submissions.publicKey,
// space: encoding.Submissions.size,
// programID: this.programID,
// }),
// await this.sys.createRentFreeAccountInstruction({
// newPubicKey: round_submissions.publicKey,
// space: encoding.Submissions.size,
// programID: this.programID,
// }),
this.instruction(Buffer.from("deadbeaf", "hex"), [
// SYSVAR_RENT_PUBKEY,
// { write: aggregator.publicKey },
// params.owner, // signed
// { write: round_submissions.publicKey },
// { write: answer_submissions.publicKey },
// SYSVAR_RENT_PUBKEY,
// { write: aggregator },
// params.owner, // signed
// { write: round_submissions },
// { write: answer_submissions },
]),
],
[
this.account,
// aggregator,
// params.owner,
// round_submissions,
// answer_submissions,
]
)
return aggregator
} }
private initializeInstruction(params: InitializeInstructionParams): TransactionInstruction { private initializeInstruction(
params: InitializeInstructionParams
): TransactionInstruction {
let { aggregator, owner } = params let { aggregator, owner } = params
const input = encoding.Initialize.serialize(params) const input = encoding.Initialize.serialize(params)
// console.log(input.toString("hex"))
return this.instruction(input, [ return this.instruction(input, [
SYSVAR_RENT_PUBKEY, SYSVAR_RENT_PUBKEY,
@ -149,21 +180,24 @@ export default class FluxAggregator extends BaseProgram {
} }
public async addOracle(params: AddOracleParams): Promise<Account> { public async addOracle(params: AddOracleParams): Promise<Account> {
const account = new Account(); const oracle = new Account()
await this.sendTx([ await this.sendTx(
await this.sys.createRentFreeAccountInstruction({ [
newPubicKey: account.publicKey, await this.sys.createRentFreeAccountInstruction({
space: OracleLayout.span, newPubicKey: oracle.publicKey,
programID: this.programID, space: encoding.Oracle.size,
}), programID: this.programID,
this.addOracleInstruction({ }),
...params, this.addOracleInstruction({
oracle: account.publicKey, ...params,
}) oracle: oracle.publicKey,
], [this.account, account, params.aggregatorOwner]); }),
],
[this.account, oracle, params.aggregatorOwner]
)
return account; return oracle
} }
public async oracleInfo(pubkey: PublicKey) { public async oracleInfo(pubkey: PublicKey) {
@ -171,99 +205,100 @@ export default class FluxAggregator extends BaseProgram {
return decodeOracleInfo(info) return decodeOracleInfo(info)
} }
private addOracleInstruction(params: AddOracleInstructionParams): TransactionInstruction { private addOracleInstruction(
const { params: AddOracleInstructionParams
oracle, ): TransactionInstruction {
owner, const { oracle, owner, description, aggregator, aggregatorOwner } = params
description,
aggregator,
aggregatorOwner,
} = params;
const layout = BufferLayout.struct([ const layout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8("instruction"),
BufferLayout.blob(32, "description"), BufferLayout.blob(32, "description"),
]); ])
return this.instructionEncode(layout, { return this.instructionEncode(
instruction: 1, // add oracle instruction layout,
description: Buffer.from(description), {
}, [ instruction: 1, // add oracle instruction
{ write: oracle }, description: Buffer.from(description),
owner, },
SYSVAR_CLOCK_PUBKEY, [
{ write: aggregator }, { write: oracle },
aggregatorOwner, owner,
]); SYSVAR_CLOCK_PUBKEY,
{ write: aggregator },
aggregatorOwner,
]
)
} }
public async removeOracle(params: RemoveOracleParams): Promise<void> { public async removeOracle(params: RemoveOracleParams): Promise<void> {
await this.sendTx([ await this.sendTx(
this.removeOracleInstruction(params) [this.removeOracleInstruction(params)],
], [this.account, params.authority || this.wallet.account]); [this.account, params.authority || this.wallet.account]
)
} }
private removeOracleInstruction(params: RemoveOracleInstructionParams): TransactionInstruction { private removeOracleInstruction(
const { params: RemoveOracleInstructionParams
authority, ): TransactionInstruction {
aggregator, const { authority, aggregator, oracle } = params
oracle,
} = params;
const layout = BufferLayout.struct([ const layout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8("instruction"),
BufferLayout.blob(32, "oracle"), BufferLayout.blob(32, "oracle"),
]); ])
return this.instructionEncode(layout, { return this.instructionEncode(
instruction: 2, // remove oracle instruction layout,
oracle: oracle.toBuffer() {
}, [ instruction: 2, // remove oracle instruction
// oracle: oracle.toBuffer(),
{ write: aggregator }, },
authority || this.wallet.account, [
]); //
{ write: aggregator },
authority || this.wallet.account,
]
)
} }
public async submit(params: SubmitParams): Promise<void> { public async submit(params: SubmitParams): Promise<void> {
await this.sendTx([ await this.sendTx(
this.submitInstruction(params) [this.submitInstruction(params)],
], [this.account, params.owner]); [this.account, params.owner]
)
} }
private submitInstruction(params: SubmitInstructionParams): TransactionInstruction { private submitInstruction(
const { params: SubmitInstructionParams
aggregator, ): TransactionInstruction {
oracle, const { aggregator, oracle, submission, owner } = params
submission,
owner,
} = params;
const layout = BufferLayout.struct([ const layout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8("instruction"),
uint64("submission"), uint64("submission"),
]); ])
return this.instructionEncode(layout, { return this.instructionEncode(
instruction: 3, // submit instruction layout,
submission: u64LEBuffer(submission), {
}, [ instruction: 3, // submit instruction
{ write: aggregator }, submission: u64LEBuffer(submission),
SYSVAR_CLOCK_PUBKEY, },
{ write: oracle }, [{ write: aggregator }, SYSVAR_CLOCK_PUBKEY, { write: oracle }, owner]
owner, )
]);
} }
public async withdraw(params: WithdrawParams): Promise<void> { public async withdraw(params: WithdrawParams): Promise<void> {
await this.sendTx([ await this.sendTx(
this.withdrawInstruction(params) [this.withdrawInstruction(params)],
], [this.account, params.authority]); [this.account, params.authority]
)
} }
private withdrawInstruction(params: WithdrawInstructionParams): TransactionInstruction { private withdrawInstruction(
params: WithdrawInstructionParams
): TransactionInstruction {
const { const {
aggregator, aggregator,
receiver, receiver,
@ -271,24 +306,27 @@ export default class FluxAggregator extends BaseProgram {
tokenOwner, tokenOwner,
tokenAccount, tokenAccount,
authority, authority,
} = params; } = params
const layout = BufferLayout.struct([ const layout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8("instruction"),
uint64("amount"), uint64("amount"),
]); ])
return this.instructionEncode(layout, { return this.instructionEncode(
instruction: 4, // withdraw instruction layout,
amount: u64LEBuffer(amount), {
}, [ instruction: 4, // withdraw instruction
{ write: aggregator }, amount: u64LEBuffer(amount),
{ write: tokenAccount }, },
{ write: receiver }, [
SPLToken.programID, { write: aggregator },
tokenOwner, { write: tokenAccount },
{ write: authority }, { write: receiver },
]); SPLToken.programID,
tokenOwner,
{ write: authority },
]
)
} }
} }

View File

@ -4,13 +4,19 @@ import fs from "fs"
import path from "path" import path from "path"
import { import {
BPFLoader, PublicKey, Wallet, NetworkName, BPFLoader,
solana, Deployer, SPLToken, ProgramAccount PublicKey,
Wallet,
NetworkName,
solana,
Deployer,
SPLToken,
ProgramAccount,
} from "solray" } from "solray"
import dotenv from "dotenv" import dotenv from "dotenv"
import FluxAggregator, { AggregatorLayout, OracleLayout } from "./FluxAggregator" import FluxAggregator from "./FluxAggregator"
import { import {
decodeAggregatorInfo, decodeAggregatorInfo,
@ -20,17 +26,20 @@ import {
} from "./utils" } from "./utils"
import * as feed from "./feed" import * as feed from "./feed"
import { AggregatorConfig } from "./schema"
dotenv.config() dotenv.config()
const cli = new Command() const cli = new Command()
const FLUX_AGGREGATOR_SO = path.resolve(__dirname, "../build/flux_aggregator.so") const FLUX_AGGREGATOR_SO = path.resolve(
__dirname,
"../build/flux_aggregator.so"
)
const network = (process.env.NETWORK || "local") as NetworkName const network = (process.env.NETWORK || "local") as NetworkName
const conn = solana.connect(network) const conn = solana.connect(network)
class AppContext { class AppContext {
static readonly AGGREGATOR_PROGRAM = "aggregatorProgram" static readonly AGGREGATOR_PROGRAM = "aggregatorProgram"
static async forAdmin() { static async forAdmin() {
@ -47,7 +56,7 @@ class AppContext {
return new AppContext(deployer, wallet) return new AppContext(deployer, wallet)
} }
constructor(public deployer: Deployer, public wallet: Wallet) { } constructor(public deployer: Deployer, public wallet: Wallet) {}
get aggregatorProgramID() { get aggregatorProgramID() {
return this.aggregatorProgramAccount.publicKey return this.aggregatorProgramAccount.publicKey
@ -70,7 +79,16 @@ class AppContext {
function color(s, c = "black", b = false): string { function color(s, c = "black", b = false): string {
// 30m Black, 31m Red, 32m Green, 33m Yellow, 34m Blue, 35m Magenta, 36m Cyanic, 37m White // 30m Black, 31m Red, 32m Green, 33m Yellow, 34m Blue, 35m Magenta, 36m Cyanic, 37m White
const cArr = ["black", "red", "green", "yellow", "blue", "megenta", "cyanic", "white"] const cArr = [
"black",
"red",
"green",
"yellow",
"blue",
"megenta",
"cyanic",
"white",
]
let cIdx = cArr.indexOf(c) let cIdx = cArr.indexOf(c)
let bold = b ? "\x1b[1m" : "" let bold = b ? "\x1b[1m" : ""
@ -89,14 +107,13 @@ function log(message: any) {
console.log(message) console.log(message)
} }
cli cli.command("generate-wallet").action(async () => {
.command("generate-wallet").action(async () => { const mnemonic = Wallet.generateMnemonic()
const mnemonic = Wallet.generateMnemonic() const wallet = await Wallet.fromMnemonic(mnemonic, conn)
const wallet = await Wallet.fromMnemonic(mnemonic, conn)
log(`address: ${wallet.address}`) log(`address: ${wallet.address}`)
log(`mnemonic: ${mnemonic}`) log(`mnemonic: ${mnemonic}`)
}) })
cli cli
.command("airdrop <address>") .command("airdrop <address>")
@ -118,40 +135,63 @@ cli
.action(async () => { .action(async () => {
const { wallet, deployer } = await AppContext.forAdmin() const { wallet, deployer } = await AppContext.forAdmin()
const programAccount = await deployer.ensure(AppContext.AGGREGATOR_PROGRAM, async () => { const programAccount = await deployer.ensure(
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO) AppContext.AGGREGATOR_PROGRAM,
async () => {
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
log(`deploying ${FLUX_AGGREGATOR_SO}...`) log(`deploying ${FLUX_AGGREGATOR_SO}...`)
const bpfLoader = new BPFLoader(wallet) const bpfLoader = new BPFLoader(wallet)
return bpfLoader.load(programBinary) return bpfLoader.load(programBinary)
}) }
)
log(`deployed aggregator program. program id: ${color(programAccount.publicKey.toBase58(), "blue")}`) log(
`deployed aggregator program. program id: ${color(
programAccount.publicKey.toBase58(),
"blue"
)}`
)
}) })
cli cli
.command("add-aggregator") .command("add-aggregator")
.description("create an aggregator") .description("create an aggregator")
.option("--feedName <string>", "feed pair name") .option("--feedName <string>", "feed pair name")
.option("--submitInterval <number>", "min wait time between submissions", "6") .option("--decimals <number>", "submission decimals", "2")
.option("--submissionDecimals <number>", "submission decimals", "12") .option("--minSubmissions <number>", "minSubmissions", "1")
.option("--minSubmissionValue <number>", "minSubmissionValue", "0") .option("--maxSubmissions <number>", "maxSubmissions", "12")
.option("--maxSubmissionValue <number>", "maxSubmissionValue", "18446744073709551615") .option("--rewardAmount <number>", "rewardAmount", "1")
.option("--restartDelay <number>", "restartDelay", "1")
.action(async (opts) => { .action(async (opts) => {
const { deployer, wallet, aggregatorProgramAccount: aggregatorProgram } = await AppContext.forAdmin() const {
deployer,
wallet,
aggregatorProgramAccount: aggregatorProgram,
} = await AppContext.forAdmin()
const { feedName, submitInterval, minSubmissionValue, maxSubmissionValue, submissionDecimals } = opts const {
feedName,
restartDelay,
minSubmissions,
maxSubmissions,
decimals,
rewardAmount,
} = opts
const aggregator = new FluxAggregator(wallet, aggregatorProgram.publicKey) const aggregator = new FluxAggregator(wallet, aggregatorProgram.publicKey)
const feed = await deployer.ensure(feedName, async () => { const feed = await deployer.ensure(feedName, async () => {
return aggregator.initialize({ return aggregator.initialize({
submitInterval: parseInt(submitInterval), config: new AggregatorConfig({
minSubmissionValue: BigInt(minSubmissionValue), description: feedName,
maxSubmissionValue: BigInt(maxSubmissionValue), decimals,
description: feedName, minSubmissions,
submissionDecimals, maxSubmissions,
restartDelay,
rewardAmount: BigInt(rewardAmount),
}),
owner: wallet.account, owner: wallet.account,
}) })
}) })
@ -200,7 +240,7 @@ cli
description: oracleName.substr(0, 32).padEnd(32), description: oracleName.substr(0, 32).padEnd(32),
aggregator: new PublicKey(feedAddress), aggregator: new PublicKey(feedAddress),
aggregatorOwner: wallet.account, aggregatorOwner: wallet.account,
}); })
log(`added oracle. pubkey: ${color(oracle.publicKey.toBase58(), "blue")}`) log(`added oracle. pubkey: ${color(oracle.publicKey.toBase58(), "blue")}`)
}) })
@ -264,10 +304,12 @@ cli
.description("oracle feeds to aggregator") .description("oracle feeds to aggregator")
.option("--feedAddress <string>", "feed address to submit values to") .option("--feedAddress <string>", "feed address to submit values to")
.option("--oracleAddress <string>", "feed address to submit values to") .option("--oracleAddress <string>", "feed address to submit values to")
.option("--pairSymbol <string>", "market pair to feed") .option("--pairSymbol <string>", "market pair to feed")
.action(async (opts) => { .action(async (opts) => {
const {
const { wallet, aggregatorProgramAccount: aggregatorProgram } = await AppContext.forOracle() wallet,
aggregatorProgramAccount: aggregatorProgram,
} = await AppContext.forOracle()
const { feedAddress, oracleAddress, pairSymbol } = opts const { feedAddress, oracleAddress, pairSymbol } = opts
@ -286,7 +328,11 @@ cli
.description("create test token") .description("create test token")
.option("--amount <number>", "amount of the test token") .option("--amount <number>", "amount of the test token")
.action(async (opts) => { .action(async (opts) => {
const { wallet, aggregatorProgramAccount: aggregatorProgram, deployer } = await AppContext.forAdmin() const {
wallet,
aggregatorProgramAccount: aggregatorProgram,
deployer,
} = await AppContext.forAdmin()
const { amount } = opts const { amount } = opts
@ -313,7 +359,7 @@ cli
// 3. create token account // 3. create token account
const tokenAccount = await spltoken.initializeAccount({ const tokenAccount = await spltoken.initializeAccount({
token: token.publicKey, token: token.publicKey,
owner: tokenOwner.pubkey owner: tokenOwner.pubkey,
}) })
log(`mint ${amount} token to token account...`) log(`mint ${amount} token to token account...`)
@ -332,10 +378,8 @@ cli
address: tokenOwner.address, address: tokenOwner.address,
seed: tokenOwner.noncedSeed.toString("hex"), seed: tokenOwner.noncedSeed.toString("hex"),
nonce: tokenOwner.nonce, nonce: tokenOwner.nonce,
} },
}) })
}) })
cli.parse(process.argv) cli.parse(process.argv)

View File

@ -2,6 +2,8 @@ import { PublicKey, Account } from "solray"
import BN from "bn.js" import BN from "bn.js"
import { deserialize, serialize } from "borsh" import { deserialize, serialize } from "borsh"
const MAX_ORACLES = 12
const boolMapper = { const boolMapper = {
encode: boolToInt, encode: boolToInt,
decode: intToBool, decode: intToBool,
@ -75,23 +77,75 @@ class Submission {
} }
} }
export class Aggregator extends Serialization { export class AggregatorConfig extends Serialization {
public submitInterval!: number public decimals!: number
public minSubmissionValue!: BN public description!: string
public maxSubmissionValue!: BN public restartDelay!: number
public submissions!: Submission[] public rewardAmount!: number
public maxSubmissions!: number
public minSubmissions!: number
public static schema = { public static schema = {
kind: "struct", kind: "struct",
fields: [ fields: [
["submitInterval", "u32"], ["description", [32], str32Mapper],
["minSubmissionValue", "u64"], ["decimals", "u8"],
["maxSubmissionValue", "u64"], ["restartDelay", "u8"],
["submissionDecimals", "u8"], ["maxSubmissions", "u8"],
["description", [32], str32Mapper], // fixed-sized-u8-array ["minSubmissions", "u8"],
["isInitialized", "u8", boolMapper], // no mapping for bool? ["rewardAmount", "u64"],
],
}
}
export class Submissions extends Serialization {
public static size = 577
public static schema = {
kind: "struct",
fields: [
["isInitialized", "u8", boolMapper],
["submissions", [MAX_ORACLES, Submission]],
],
}
}
class Round extends Serialization {
public static schema = {
kind: "struct",
fields: [
["id", "u64"],
["created_at", "u64"],
["updated_at", "u64"],
],
}
}
class Answer extends Serialization {
public static schema = {
kind: "struct",
fields: [
["round_id", "u64"],
["created_at", "u64"],
["updated_at", "u64"],
],
}
}
export class Aggregator extends Serialization {
public static size = 189
public config!: AggregatorConfig
// public submissions!: Submission[]
public static schema = {
kind: "struct",
fields: [
["config", AggregatorConfig],
["isInitialized", "u8", boolMapper],
["owner", [32], pubkeyMapper], ["owner", [32], pubkeyMapper],
["submissions", [Submission, 12]], ["round", Round],
["round_submissions", [32], pubkeyMapper],
["answer", Answer],
["answer_submissions", [32], pubkeyMapper],
], ],
} }
} }
@ -103,22 +157,16 @@ abstract class InstructionSerialization extends Serialization {
} }
export class Initialize extends InstructionSerialization { export class Initialize extends InstructionSerialization {
public submitInterval!: number // public submitInterval!: number
public minSubmissionValue!: number // public minSubmissionValue!: number
public maxSubmissionValue!: number // public maxSubmissionValue!: number
public submissionDecimals!: number // public submissionDecimals!: number
/// A short description of what is being reported // /// A short description of what is being reported
public description!: string // public description!: string
public static schema = { public static schema = {
kind: "struct", kind: "struct",
fields: [ fields: [["config", AggregatorConfig]],
["submitInterval", "u32"],
["minSubmissionValue", "u64"],
["maxSubmissionValue", "u64"],
["submissionDecimals", "u8"],
["description", [32], str32Mapper],
],
} }
} }
@ -167,12 +215,18 @@ function boolToInt(t: boolean) {
} }
} }
export class Oracle {
public static size = 113
}
// if there is optional or variable length items, what is: borsh_utils::get_packed_len::<Submission>()? // if there is optional or variable length items, what is: borsh_utils::get_packed_len::<Submission>()?
// //
// would panic given variable sized types // would panic given variable sized types
export const schema = new Map([ export const schema = new Map([
[Aggregator, Aggregator.schema], [Aggregator, Aggregator.schema],
[AggregatorConfig, AggregatorConfig.schema],
[Submissions, Submissions.schema],
[Submission, Submission.schema], [Submission, Submission.schema],
[Initialize, Initialize.schema], [Initialize, Initialize.schema],
[Instruction, Instruction.schema], [Instruction, Instruction.schema],

View File

@ -1,6 +1,10 @@
import { Connection, PublicKey } from "@solana/web3.js" import { Connection, PublicKey } from "@solana/web3.js"
import { AggregatorLayout, SubmissionLayout, OracleLayout } from "./FluxAggregator" import {
AggregatorLayout,
SubmissionLayout,
OracleLayout,
} from "./FluxAggregator"
import { solana, Wallet, NetworkName, Deployer } from "solray" import { solana, Wallet, NetworkName, Deployer } from "solray"
@ -17,9 +21,8 @@ export function getMedian(submissions: number[]): number {
return values[0] return values[0]
} else { } else {
let i = len / 2 let i = len / 2
return len % 2 == 0 ? (values[i] + values[i-1])/2 : values[i] return len % 2 == 0 ? (values[i] + values[i - 1]) / 2 : values[i]
} }
} }
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
@ -46,7 +49,10 @@ export function decodeAggregatorInfo(accountInfo) {
for (let i = 0; i < aggregator.submissions.length / submissionSpace; i++) { for (let i = 0; i < aggregator.submissions.length / submissionSpace; i++) {
let submission = SubmissionLayout.decode( let submission = SubmissionLayout.decode(
aggregator.submissions.slice(i*submissionSpace, (i+1)*submissionSpace) aggregator.submissions.slice(
i * submissionSpace,
(i + 1) * submissionSpace
)
) )
submission.oracle = new PublicKey(submission.oracle) submission.oracle = new PublicKey(submission.oracle)
@ -68,8 +74,8 @@ export function decodeAggregatorInfo(accountInfo) {
submissionValue: getMedian(submissions), submissionValue: getMedian(submissions),
submitInterval, submitInterval,
description, description,
oracles: submissions.map(s => s.oracle.toString()), oracles: submissions.map((s) => s.oracle.toString()),
latestUpdateTime: new Date(Number(latestUpdateTime)*1000), latestUpdateTime: new Date(Number(latestUpdateTime) * 1000),
} }
} }
@ -88,7 +94,10 @@ export function decodeOracleInfo(accountInfo) {
return oracle return oracle
} }
export async function walletFromEnv(key: string, conn: Connection): Promise<Wallet> { export async function walletFromEnv(
key: string,
conn: Connection
): Promise<Wallet> {
const mnemonic = process.env[key] const mnemonic = process.env[key]
if (!mnemonic) { if (!mnemonic) {
throw new Error(`Set ${key} in .env to be a mnemonic`) throw new Error(`Set ${key} in .env to be a mnemonic`)