wtf
This commit is contained in:
parent
01a910e7aa
commit
7a5112147f
10
README.md
10
README.md
|
@ -1,6 +1,6 @@
|
|||
# solana-flux-aggregator
|
||||
|
||||
Solnana Flux Aggregator
|
||||
Solnana Flux Aggregator
|
||||
|
||||
Price Feeds: [https://sol.link](https://sol.link)
|
||||
|
||||
|
@ -51,11 +51,7 @@ Create the `btc:usd` feed (that accepts max and min u64 as valid submission valu
|
|||
|
||||
```
|
||||
yarn solink add-aggregator \
|
||||
--feedName btc:usd \
|
||||
--submitInterval 6 \
|
||||
--minSubmissionValue 0 \
|
||||
--maxSubmissionValue 18446744073709551615 \
|
||||
--submissionDecimals 2
|
||||
--feedName btc:usd
|
||||
|
||||
feed initialized, pubkey: 3aTBom2uodyWkuVPiUkwCZ2HiFywdUx9tp7su7U2H4Nx
|
||||
```
|
||||
|
@ -137,4 +133,4 @@ use flux_aggregator;
|
|||
|
||||
let feed_info = next_account_info(accounts_iter)?;
|
||||
let value = flux_aggregator::get_median(feed_info)?;
|
||||
```
|
||||
```
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@solana/web3.js": "^0.87.1",
|
||||
"@solana/web3.js": "^0.90.5",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"commander": "^6.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
|
|
|
@ -16,10 +16,10 @@ thiserror = "1.0"
|
|||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
num_enum = "0.5.1"
|
||||
hex = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-sdk = "1.4.8"
|
||||
hex = "0.4"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
//! Program entrypoint
|
||||
|
||||
use crate::{processor::Processor};
|
||||
use crate::processor::Processor;
|
||||
|
||||
use solana_program::{
|
||||
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
|
||||
msg,
|
||||
};
|
||||
|
||||
use hex;
|
||||
|
||||
entrypoint!(process_instruction);
|
||||
|
||||
// Program entrypoint's implementation
|
||||
|
@ -14,11 +17,12 @@ fn process_instruction<'a>(
|
|||
accounts: &'a [AccountInfo<'a>],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
|
||||
// catch the error so we can print it
|
||||
// error.print::<Error>();
|
||||
// msg!("{:?}", error);
|
||||
return Err(error);
|
||||
}
|
||||
msg!("instruction: {}", hex::encode(instruction_data));
|
||||
// if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
|
||||
// // catch the error so we can print it
|
||||
// // error.print::<Error>();
|
||||
// // msg!("{:?}", error);
|
||||
// return Err(error);
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -38,3 +38,23 @@ pub enum Instruction {
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,8 +42,8 @@ impl<'a, 'b> Accounts<'a, 'b> {
|
|||
struct InitializeContext<'a> {
|
||||
rent: Rent,
|
||||
aggregator: &'a AccountInfo<'a>,
|
||||
aggregator_owner: &'a AccountInfo<'a>,
|
||||
round_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
|
||||
aggregator_owner: &'a AccountInfo<'a>, // signed
|
||||
round_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
|
||||
answer_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
|
||||
|
||||
config: AggregatorConfig,
|
||||
|
|
|
@ -187,3 +187,24 @@ impl IsInitialized 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>());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import {
|
||||
PublicKey, BaseProgram, Account,
|
||||
Wallet, System, SPLToken
|
||||
} from "solray";
|
||||
PublicKey,
|
||||
BaseProgram,
|
||||
Account,
|
||||
Wallet,
|
||||
System,
|
||||
SPLToken,
|
||||
} from "solray"
|
||||
|
||||
import {
|
||||
SYSVAR_RENT_PUBKEY, SYSVAR_CLOCK_PUBKEY,
|
||||
TransactionInstruction, SystemProgram
|
||||
} from "@solana/web3.js";
|
||||
|
||||
import { publicKey, u64LEBuffer, uint64, BufferLayout } from "solray/lib/util/encoding";
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
TransactionInstruction,
|
||||
SystemProgram,
|
||||
} from "@solana/web3.js"
|
||||
|
||||
import {
|
||||
decodeOracleInfo
|
||||
} from "./utils"
|
||||
publicKey,
|
||||
u64LEBuffer,
|
||||
uint64,
|
||||
BufferLayout,
|
||||
} from "solray/lib/util/encoding"
|
||||
|
||||
import { decodeOracleInfo } from "./utils"
|
||||
|
||||
// @ts-ignore
|
||||
// import BufferLayout from "buffer-layout";
|
||||
|
@ -21,125 +30,147 @@ import { schema } from "./schema"
|
|||
import * as encoding from "./schema"
|
||||
import { deserialize, serialize } from "borsh"
|
||||
|
||||
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 AggregatorLayout = BufferLayout.struct([])
|
||||
|
||||
export const OracleLayout = BufferLayout.struct([
|
||||
uint64("nextSubmitTime"),
|
||||
BufferLayout.blob(32, "description"),
|
||||
BufferLayout.u8("isInitialized"),
|
||||
uint64("withdrawable"),
|
||||
publicKey('aggregator'),
|
||||
publicKey('owner'),
|
||||
]);
|
||||
publicKey("aggregator"),
|
||||
publicKey("owner"),
|
||||
])
|
||||
|
||||
export const SubmissionLayout = BufferLayout.struct([
|
||||
uint64("time"),
|
||||
uint64("value"),
|
||||
publicKey('oracle'),
|
||||
]);
|
||||
publicKey("oracle"),
|
||||
])
|
||||
|
||||
interface InitializeParams {
|
||||
submitInterval: number;
|
||||
minSubmissionValue: bigint;
|
||||
maxSubmissionValue: bigint;
|
||||
submissionDecimals: number;
|
||||
description: string;
|
||||
owner: Account;
|
||||
config: encoding.AggregatorConfig
|
||||
owner: Account
|
||||
}
|
||||
|
||||
interface InitializeInstructionParams extends InitializeParams {
|
||||
aggregator: PublicKey;
|
||||
aggregator: PublicKey
|
||||
}
|
||||
|
||||
interface AddOracleParams {
|
||||
owner: PublicKey;
|
||||
description: string;
|
||||
aggregator: PublicKey;
|
||||
owner: PublicKey
|
||||
description: string
|
||||
aggregator: PublicKey
|
||||
// To prove you are the aggregator owner
|
||||
aggregatorOwner: Account;
|
||||
aggregatorOwner: Account
|
||||
}
|
||||
|
||||
interface AddOracleInstructionParams extends AddOracleParams {
|
||||
oracle: PublicKey;
|
||||
oracle: PublicKey
|
||||
}
|
||||
|
||||
interface RemoveOracleParams {
|
||||
aggregator: PublicKey;
|
||||
oracle: PublicKey;
|
||||
aggregator: PublicKey
|
||||
oracle: PublicKey
|
||||
// To prove you are the aggregator owner
|
||||
authority?: Account;
|
||||
authority?: Account
|
||||
}
|
||||
|
||||
interface RemoveOracleInstructionParams extends RemoveOracleParams {
|
||||
}
|
||||
interface RemoveOracleInstructionParams extends RemoveOracleParams {}
|
||||
|
||||
interface SubmitParams {
|
||||
aggregator: PublicKey;
|
||||
oracle: PublicKey;
|
||||
aggregator: PublicKey
|
||||
oracle: PublicKey
|
||||
// The oracle"s index
|
||||
submission: bigint;
|
||||
submission: bigint
|
||||
// oracle owner
|
||||
owner: Account;
|
||||
owner: Account
|
||||
}
|
||||
|
||||
interface SubmitInstructionParams extends SubmitParams {
|
||||
}
|
||||
interface SubmitInstructionParams extends SubmitParams {}
|
||||
|
||||
interface WithdrawParams {
|
||||
aggregator: PublicKey,
|
||||
aggregator: PublicKey
|
||||
// withdraw to
|
||||
receiver: PublicKey,
|
||||
receiver: PublicKey
|
||||
// withdraw amount
|
||||
amount: bigint,
|
||||
tokenAccount: PublicKey,
|
||||
tokenOwner: PublicKey,
|
||||
amount: bigint
|
||||
tokenAccount: PublicKey
|
||||
tokenOwner: PublicKey
|
||||
// signer
|
||||
authority: Account,
|
||||
authority: Account
|
||||
}
|
||||
|
||||
interface WithdrawInstructionParams extends WithdrawParams {
|
||||
}
|
||||
interface WithdrawInstructionParams extends WithdrawParams {}
|
||||
|
||||
export default class FluxAggregator extends BaseProgram {
|
||||
|
||||
private sys: System
|
||||
constructor(wallet: Wallet, programID: PublicKey) {
|
||||
super(wallet, programID);
|
||||
this.sys = new System(this.wallet);
|
||||
super(wallet, programID)
|
||||
this.sys = new System(this.wallet)
|
||||
}
|
||||
|
||||
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([
|
||||
await this.sys.createRentFreeAccountInstruction({
|
||||
newPubicKey: account.publicKey,
|
||||
space: AggregatorLayout.span,
|
||||
programID: this.programID,
|
||||
}),
|
||||
this.initializeInstruction({
|
||||
...params,
|
||||
aggregator: account.publicKey,
|
||||
})
|
||||
], [this.account, account, params.owner]);
|
||||
console.log({ config: params.config, program: this.programID.toString() })
|
||||
|
||||
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
|
||||
|
||||
const input = encoding.Initialize.serialize(params)
|
||||
// console.log(input.toString("hex"))
|
||||
|
||||
return this.instruction(input, [
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
|
@ -149,21 +180,24 @@ export default class FluxAggregator extends BaseProgram {
|
|||
}
|
||||
|
||||
public async addOracle(params: AddOracleParams): Promise<Account> {
|
||||
const account = new Account();
|
||||
const oracle = new Account()
|
||||
|
||||
await this.sendTx([
|
||||
await this.sys.createRentFreeAccountInstruction({
|
||||
newPubicKey: account.publicKey,
|
||||
space: OracleLayout.span,
|
||||
programID: this.programID,
|
||||
}),
|
||||
this.addOracleInstruction({
|
||||
...params,
|
||||
oracle: account.publicKey,
|
||||
})
|
||||
], [this.account, account, params.aggregatorOwner]);
|
||||
await this.sendTx(
|
||||
[
|
||||
await this.sys.createRentFreeAccountInstruction({
|
||||
newPubicKey: oracle.publicKey,
|
||||
space: encoding.Oracle.size,
|
||||
programID: this.programID,
|
||||
}),
|
||||
this.addOracleInstruction({
|
||||
...params,
|
||||
oracle: oracle.publicKey,
|
||||
}),
|
||||
],
|
||||
[this.account, oracle, params.aggregatorOwner]
|
||||
)
|
||||
|
||||
return account;
|
||||
return oracle
|
||||
}
|
||||
|
||||
public async oracleInfo(pubkey: PublicKey) {
|
||||
|
@ -171,99 +205,100 @@ export default class FluxAggregator extends BaseProgram {
|
|||
return decodeOracleInfo(info)
|
||||
}
|
||||
|
||||
private addOracleInstruction(params: AddOracleInstructionParams): TransactionInstruction {
|
||||
const {
|
||||
oracle,
|
||||
owner,
|
||||
description,
|
||||
aggregator,
|
||||
aggregatorOwner,
|
||||
} = params;
|
||||
private addOracleInstruction(
|
||||
params: AddOracleInstructionParams
|
||||
): TransactionInstruction {
|
||||
const { oracle, owner, description, aggregator, aggregatorOwner } = params
|
||||
|
||||
const layout = BufferLayout.struct([
|
||||
BufferLayout.u8("instruction"),
|
||||
BufferLayout.blob(32, "description"),
|
||||
]);
|
||||
])
|
||||
|
||||
return this.instructionEncode(layout, {
|
||||
instruction: 1, // add oracle instruction
|
||||
description: Buffer.from(description),
|
||||
}, [
|
||||
{ write: oracle },
|
||||
owner,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
{ write: aggregator },
|
||||
aggregatorOwner,
|
||||
]);
|
||||
return this.instructionEncode(
|
||||
layout,
|
||||
{
|
||||
instruction: 1, // add oracle instruction
|
||||
description: Buffer.from(description),
|
||||
},
|
||||
[
|
||||
{ write: oracle },
|
||||
owner,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
{ write: aggregator },
|
||||
aggregatorOwner,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
public async removeOracle(params: RemoveOracleParams): Promise<void> {
|
||||
await this.sendTx([
|
||||
this.removeOracleInstruction(params)
|
||||
], [this.account, params.authority || this.wallet.account]);
|
||||
await this.sendTx(
|
||||
[this.removeOracleInstruction(params)],
|
||||
[this.account, params.authority || this.wallet.account]
|
||||
)
|
||||
}
|
||||
|
||||
private removeOracleInstruction(params: RemoveOracleInstructionParams): TransactionInstruction {
|
||||
const {
|
||||
authority,
|
||||
aggregator,
|
||||
oracle,
|
||||
} = params;
|
||||
private removeOracleInstruction(
|
||||
params: RemoveOracleInstructionParams
|
||||
): TransactionInstruction {
|
||||
const { authority, aggregator, oracle } = params
|
||||
|
||||
const layout = BufferLayout.struct([
|
||||
BufferLayout.u8("instruction"),
|
||||
BufferLayout.blob(32, "oracle"),
|
||||
]);
|
||||
])
|
||||
|
||||
return this.instructionEncode(layout, {
|
||||
instruction: 2, // remove oracle instruction
|
||||
oracle: oracle.toBuffer()
|
||||
}, [
|
||||
//
|
||||
{ write: aggregator },
|
||||
authority || this.wallet.account,
|
||||
]);
|
||||
return this.instructionEncode(
|
||||
layout,
|
||||
{
|
||||
instruction: 2, // remove oracle instruction
|
||||
oracle: oracle.toBuffer(),
|
||||
},
|
||||
[
|
||||
//
|
||||
{ write: aggregator },
|
||||
authority || this.wallet.account,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
public async submit(params: SubmitParams): Promise<void> {
|
||||
await this.sendTx([
|
||||
this.submitInstruction(params)
|
||||
], [this.account, params.owner]);
|
||||
|
||||
await this.sendTx(
|
||||
[this.submitInstruction(params)],
|
||||
[this.account, params.owner]
|
||||
)
|
||||
}
|
||||
|
||||
private submitInstruction(params: SubmitInstructionParams): TransactionInstruction {
|
||||
const {
|
||||
aggregator,
|
||||
oracle,
|
||||
submission,
|
||||
owner,
|
||||
} = params;
|
||||
private submitInstruction(
|
||||
params: SubmitInstructionParams
|
||||
): TransactionInstruction {
|
||||
const { aggregator, oracle, submission, owner } = params
|
||||
|
||||
const layout = BufferLayout.struct([
|
||||
BufferLayout.u8("instruction"),
|
||||
uint64("submission"),
|
||||
]);
|
||||
])
|
||||
|
||||
return this.instructionEncode(layout, {
|
||||
instruction: 3, // submit instruction
|
||||
submission: u64LEBuffer(submission),
|
||||
}, [
|
||||
{ write: aggregator },
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
{ write: oracle },
|
||||
owner,
|
||||
]);
|
||||
return this.instructionEncode(
|
||||
layout,
|
||||
{
|
||||
instruction: 3, // submit instruction
|
||||
submission: u64LEBuffer(submission),
|
||||
},
|
||||
[{ write: aggregator }, SYSVAR_CLOCK_PUBKEY, { write: oracle }, owner]
|
||||
)
|
||||
}
|
||||
|
||||
public async withdraw(params: WithdrawParams): Promise<void> {
|
||||
await this.sendTx([
|
||||
this.withdrawInstruction(params)
|
||||
], [this.account, params.authority]);
|
||||
|
||||
await this.sendTx(
|
||||
[this.withdrawInstruction(params)],
|
||||
[this.account, params.authority]
|
||||
)
|
||||
}
|
||||
|
||||
private withdrawInstruction(params: WithdrawInstructionParams): TransactionInstruction {
|
||||
private withdrawInstruction(
|
||||
params: WithdrawInstructionParams
|
||||
): TransactionInstruction {
|
||||
const {
|
||||
aggregator,
|
||||
receiver,
|
||||
|
@ -271,24 +306,27 @@ export default class FluxAggregator extends BaseProgram {
|
|||
tokenOwner,
|
||||
tokenAccount,
|
||||
authority,
|
||||
} = params;
|
||||
} = params
|
||||
|
||||
const layout = BufferLayout.struct([
|
||||
BufferLayout.u8("instruction"),
|
||||
uint64("amount"),
|
||||
]);
|
||||
])
|
||||
|
||||
return this.instructionEncode(layout, {
|
||||
instruction: 4, // withdraw instruction
|
||||
amount: u64LEBuffer(amount),
|
||||
}, [
|
||||
{ write: aggregator },
|
||||
{ write: tokenAccount },
|
||||
{ write: receiver },
|
||||
SPLToken.programID,
|
||||
tokenOwner,
|
||||
{ write: authority },
|
||||
]);
|
||||
return this.instructionEncode(
|
||||
layout,
|
||||
{
|
||||
instruction: 4, // withdraw instruction
|
||||
amount: u64LEBuffer(amount),
|
||||
},
|
||||
[
|
||||
{ write: aggregator },
|
||||
{ write: tokenAccount },
|
||||
{ write: receiver },
|
||||
SPLToken.programID,
|
||||
tokenOwner,
|
||||
{ write: authority },
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
128
src/cli.ts
128
src/cli.ts
|
@ -4,13 +4,19 @@ import fs from "fs"
|
|||
import path from "path"
|
||||
|
||||
import {
|
||||
BPFLoader, PublicKey, Wallet, NetworkName,
|
||||
solana, Deployer, SPLToken, ProgramAccount
|
||||
BPFLoader,
|
||||
PublicKey,
|
||||
Wallet,
|
||||
NetworkName,
|
||||
solana,
|
||||
Deployer,
|
||||
SPLToken,
|
||||
ProgramAccount,
|
||||
} from "solray"
|
||||
|
||||
import dotenv from "dotenv"
|
||||
|
||||
import FluxAggregator, { AggregatorLayout, OracleLayout } from "./FluxAggregator"
|
||||
import FluxAggregator from "./FluxAggregator"
|
||||
|
||||
import {
|
||||
decodeAggregatorInfo,
|
||||
|
@ -20,17 +26,20 @@ import {
|
|||
} from "./utils"
|
||||
|
||||
import * as feed from "./feed"
|
||||
import { AggregatorConfig } from "./schema"
|
||||
|
||||
dotenv.config()
|
||||
|
||||
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 conn = solana.connect(network)
|
||||
|
||||
class AppContext {
|
||||
|
||||
static readonly AGGREGATOR_PROGRAM = "aggregatorProgram"
|
||||
|
||||
static async forAdmin() {
|
||||
|
@ -47,7 +56,7 @@ class AppContext {
|
|||
return new AppContext(deployer, wallet)
|
||||
}
|
||||
|
||||
constructor(public deployer: Deployer, public wallet: Wallet) { }
|
||||
constructor(public deployer: Deployer, public wallet: Wallet) {}
|
||||
|
||||
get aggregatorProgramID() {
|
||||
return this.aggregatorProgramAccount.publicKey
|
||||
|
@ -70,7 +79,16 @@ class AppContext {
|
|||
|
||||
function color(s, c = "black", b = false): string {
|
||||
// 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 bold = b ? "\x1b[1m" : ""
|
||||
|
@ -89,14 +107,13 @@ function log(message: any) {
|
|||
console.log(message)
|
||||
}
|
||||
|
||||
cli
|
||||
.command("generate-wallet").action(async () => {
|
||||
const mnemonic = Wallet.generateMnemonic()
|
||||
const wallet = await Wallet.fromMnemonic(mnemonic, conn)
|
||||
cli.command("generate-wallet").action(async () => {
|
||||
const mnemonic = Wallet.generateMnemonic()
|
||||
const wallet = await Wallet.fromMnemonic(mnemonic, conn)
|
||||
|
||||
log(`address: ${wallet.address}`)
|
||||
log(`mnemonic: ${mnemonic}`)
|
||||
})
|
||||
log(`address: ${wallet.address}`)
|
||||
log(`mnemonic: ${mnemonic}`)
|
||||
})
|
||||
|
||||
cli
|
||||
.command("airdrop <address>")
|
||||
|
@ -118,40 +135,63 @@ cli
|
|||
.action(async () => {
|
||||
const { wallet, deployer } = await AppContext.forAdmin()
|
||||
|
||||
const programAccount = await deployer.ensure(AppContext.AGGREGATOR_PROGRAM, async () => {
|
||||
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
|
||||
const programAccount = await deployer.ensure(
|
||||
AppContext.AGGREGATOR_PROGRAM,
|
||||
async () => {
|
||||
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
|
||||
|
||||
log(`deploying ${FLUX_AGGREGATOR_SO}...`)
|
||||
const bpfLoader = new BPFLoader(wallet)
|
||||
log(`deploying ${FLUX_AGGREGATOR_SO}...`)
|
||||
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
|
||||
.command("add-aggregator")
|
||||
.description("create an aggregator")
|
||||
.option("--feedName <string>", "feed pair name")
|
||||
.option("--submitInterval <number>", "min wait time between submissions", "6")
|
||||
.option("--submissionDecimals <number>", "submission decimals", "12")
|
||||
.option("--minSubmissionValue <number>", "minSubmissionValue", "0")
|
||||
.option("--maxSubmissionValue <number>", "maxSubmissionValue", "18446744073709551615")
|
||||
.option("--decimals <number>", "submission decimals", "2")
|
||||
.option("--minSubmissions <number>", "minSubmissions", "1")
|
||||
.option("--maxSubmissions <number>", "maxSubmissions", "12")
|
||||
.option("--rewardAmount <number>", "rewardAmount", "1")
|
||||
.option("--restartDelay <number>", "restartDelay", "1")
|
||||
.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 feed = await deployer.ensure(feedName, async () => {
|
||||
return aggregator.initialize({
|
||||
submitInterval: parseInt(submitInterval),
|
||||
minSubmissionValue: BigInt(minSubmissionValue),
|
||||
maxSubmissionValue: BigInt(maxSubmissionValue),
|
||||
description: feedName,
|
||||
submissionDecimals,
|
||||
config: new AggregatorConfig({
|
||||
description: feedName,
|
||||
decimals,
|
||||
minSubmissions,
|
||||
maxSubmissions,
|
||||
restartDelay,
|
||||
rewardAmount: BigInt(rewardAmount),
|
||||
}),
|
||||
owner: wallet.account,
|
||||
})
|
||||
})
|
||||
|
@ -200,7 +240,7 @@ cli
|
|||
description: oracleName.substr(0, 32).padEnd(32),
|
||||
aggregator: new PublicKey(feedAddress),
|
||||
aggregatorOwner: wallet.account,
|
||||
});
|
||||
})
|
||||
|
||||
log(`added oracle. pubkey: ${color(oracle.publicKey.toBase58(), "blue")}`)
|
||||
})
|
||||
|
@ -264,10 +304,12 @@ cli
|
|||
.description("oracle feeds to aggregator")
|
||||
.option("--feedAddress <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) => {
|
||||
|
||||
const { wallet, aggregatorProgramAccount: aggregatorProgram } = await AppContext.forOracle()
|
||||
const {
|
||||
wallet,
|
||||
aggregatorProgramAccount: aggregatorProgram,
|
||||
} = await AppContext.forOracle()
|
||||
|
||||
const { feedAddress, oracleAddress, pairSymbol } = opts
|
||||
|
||||
|
@ -286,7 +328,11 @@ cli
|
|||
.description("create test token")
|
||||
.option("--amount <number>", "amount of the test token")
|
||||
.action(async (opts) => {
|
||||
const { wallet, aggregatorProgramAccount: aggregatorProgram, deployer } = await AppContext.forAdmin()
|
||||
const {
|
||||
wallet,
|
||||
aggregatorProgramAccount: aggregatorProgram,
|
||||
deployer,
|
||||
} = await AppContext.forAdmin()
|
||||
|
||||
const { amount } = opts
|
||||
|
||||
|
@ -313,7 +359,7 @@ cli
|
|||
// 3. create token account
|
||||
const tokenAccount = await spltoken.initializeAccount({
|
||||
token: token.publicKey,
|
||||
owner: tokenOwner.pubkey
|
||||
owner: tokenOwner.pubkey,
|
||||
})
|
||||
|
||||
log(`mint ${amount} token to token account...`)
|
||||
|
@ -332,10 +378,8 @@ cli
|
|||
address: tokenOwner.address,
|
||||
seed: tokenOwner.noncedSeed.toString("hex"),
|
||||
nonce: tokenOwner.nonce,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
cli.parse(process.argv)
|
||||
cli.parse(process.argv)
|
||||
|
|
104
src/schema.ts
104
src/schema.ts
|
@ -2,6 +2,8 @@ import { PublicKey, Account } from "solray"
|
|||
import BN from "bn.js"
|
||||
import { deserialize, serialize } from "borsh"
|
||||
|
||||
const MAX_ORACLES = 12
|
||||
|
||||
const boolMapper = {
|
||||
encode: boolToInt,
|
||||
decode: intToBool,
|
||||
|
@ -75,23 +77,75 @@ class Submission {
|
|||
}
|
||||
}
|
||||
|
||||
export class Aggregator extends Serialization {
|
||||
public submitInterval!: number
|
||||
public minSubmissionValue!: BN
|
||||
public maxSubmissionValue!: BN
|
||||
public submissions!: Submission[]
|
||||
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: [
|
||||
["submitInterval", "u32"],
|
||||
["minSubmissionValue", "u64"],
|
||||
["maxSubmissionValue", "u64"],
|
||||
["submissionDecimals", "u8"],
|
||||
["description", [32], str32Mapper], // fixed-sized-u8-array
|
||||
["isInitialized", "u8", boolMapper], // no mapping for bool?
|
||||
["description", [32], str32Mapper],
|
||||
["decimals", "u8"],
|
||||
["restartDelay", "u8"],
|
||||
["maxSubmissions", "u8"],
|
||||
["minSubmissions", "u8"],
|
||||
["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],
|
||||
["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 {
|
||||
public submitInterval!: number
|
||||
public minSubmissionValue!: number
|
||||
public maxSubmissionValue!: number
|
||||
public submissionDecimals!: number
|
||||
/// A short description of what is being reported
|
||||
public description!: string
|
||||
// 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: [
|
||||
["submitInterval", "u32"],
|
||||
["minSubmissionValue", "u64"],
|
||||
["maxSubmissionValue", "u64"],
|
||||
["submissionDecimals", "u8"],
|
||||
["description", [32], str32Mapper],
|
||||
],
|
||||
fields: [["config", AggregatorConfig]],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>()?
|
||||
//
|
||||
// would panic given variable sized types
|
||||
|
||||
export const schema = new Map([
|
||||
[Aggregator, Aggregator.schema],
|
||||
[AggregatorConfig, AggregatorConfig.schema],
|
||||
[Submissions, Submissions.schema],
|
||||
[Submission, Submission.schema],
|
||||
[Initialize, Initialize.schema],
|
||||
[Instruction, Instruction.schema],
|
||||
|
|
25
src/utils.ts
25
src/utils.ts
|
@ -1,6 +1,10 @@
|
|||
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"
|
||||
|
||||
|
@ -17,9 +21,8 @@ export function getMedian(submissions: number[]): number {
|
|||
return values[0]
|
||||
} else {
|
||||
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> {
|
||||
|
@ -46,7 +49,10 @@ export function decodeAggregatorInfo(accountInfo) {
|
|||
|
||||
for (let i = 0; i < aggregator.submissions.length / submissionSpace; i++) {
|
||||
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)
|
||||
|
@ -68,8 +74,8 @@ export function decodeAggregatorInfo(accountInfo) {
|
|||
submissionValue: getMedian(submissions),
|
||||
submitInterval,
|
||||
description,
|
||||
oracles: submissions.map(s => s.oracle.toString()),
|
||||
latestUpdateTime: new Date(Number(latestUpdateTime)*1000),
|
||||
oracles: submissions.map((s) => s.oracle.toString()),
|
||||
latestUpdateTime: new Date(Number(latestUpdateTime) * 1000),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +94,10 @@ export function decodeOracleInfo(accountInfo) {
|
|||
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]
|
||||
if (!mnemonic) {
|
||||
throw new Error(`Set ${key} in .env to be a mnemonic`)
|
||||
|
@ -107,4 +116,4 @@ export async function openDeployer(): Promise<Deployer> {
|
|||
}
|
||||
|
||||
return Deployer.open(deployFile)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue