refactor cli

This commit is contained in:
defactojob@tutanota.com 2020-12-09 11:41:15 +00:00
parent 7749dff9ea
commit faa0f8e8c6
13 changed files with 1776 additions and 598 deletions

5
.env
View File

@ -1 +1,4 @@
NETWORK=local
NETWORK=dev
DEPLOY_FILE=deploy.json
ADMIN_MNEMONIC="wine vault fancy enhance trade dolphin hard traffic social butter client pave"
ORACLE_MNEMONIC="amount smoke bar coil current trial toward minimum model pass moral liberty"

View File

@ -1,44 +1,96 @@
# solana-flux-aggregator
Solnana Flux Aggregator
## Install
`npm install`
```
yarn install
```
`sudo npm run build:program`
## Admin Wallet Setup
## Create Payer
Setup a wallet for the flux aggregator admin:
`npm run create:payer`
```
yarn generate-wallet
request some airdrop:
address: 7YMUUCzZir7AAuoy4CtZih9JFBqYwtQiCxjA5dtqwRxU
mnemonic: wine vault fancy enhance trade dolphin hard traffic social butter client pave
```
`npm run airdrop:payer`
```
yarn solink airdrop 7YMUUCzZir7AAuoy4CtZih9JFBqYwtQiCxjA5dtqwRxU
```
## Deploy
Create `.env` configuration file for the deploy script.
`npm run deploy`
```
NETWORK=dev
DEPLOY_FILE=deploy.json
ADMIN_MNEMONIC="wine vault fancy enhance trade dolphin hard traffic social butter client pave"
```
## Create Aggregator Owner
## Aggregator Setup
`npm run create:aggregatorOwner`
Build and deploy the flux aggregator:
## Add An Aggregator
```
yarn build:program
```
`npm run add:aggregator`
```
yarn solink deploy-program
## Create Oracle Owner
deployed aggregator program. program id: 9KXbVqUrMgtti7Jx4rrV1NqXjQNxWaKgtYCEwJ8AESS5
```
`npm run create:oracleOwner`
Create the `btc:usd` feed (that accepts max and min u64 as valid submission values):
## Add An Oracle
```
yarn solink add-aggregator \
--feedName btc:usd \
--submitInterval 6 \
--minSubmissionValue 0 \
--maxSubmissionValue 18446744073709551615
`npm run add:oracle`
feed initialized, pubkey: AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2
```
## Show Aggregators/Oracles
## Adding an oracle
`npm run show:aggregators|oracles`
Next, we create a separate wallet to control oracles:
## Start to feed
```
yarn generate-wallet
`npm run feed`
address: FosLwbttPgkEDv36VJLU3wwXcBSSoUGkh7dyZPsXNtT4
mnemonic: amount smoke bar coil current trial toward minimum model pass moral liberty
```
```
yarn solink airdrop FosLwbttPgkEDv36VJLU3wwXcBSSoUGkh7dyZPsXNtT4
```
Add this wallet to `.env`:
```
ORACLE_MNEMONIC="amount smoke bar coil current trial toward minimum model pass moral liberty"
```
Next we create a new oracle to the feed we've created previously, and set its owner to be the new oracle wallet that we've generated:
```
yarn solink add-oracle \
--feedAddress AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2 \
--oracleName solink-test \
--oracleOwner FosLwbttPgkEDv36VJLU3wwXcBSSoUGkh7dyZPsXNtT4
added oracle. pubkey: 4vH5L2jSNXGfcCx42N4sqPiMzEbp1PaQjQ6XngDBu8zR
```
```
yarn solink feed \
--feedAddress AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2 \
--oracleAddress 4vH5L2jSNXGfcCx42N4sqPiMzEbp1PaQjQ6XngDBu8zR
```

10
deploy.json Normal file
View File

@ -0,0 +1,10 @@
{
"aggregatorProgram": {
"pubkey": "9KXbVqUrMgtti7Jx4rrV1NqXjQNxWaKgtYCEwJ8AESS5",
"secret": "a2b92fa9514ac18a40c2bc895db0f97cabc1870205a74d84a0fb4a8167e6cba37b9c8b296cb261a7f3b17b294c72c4b6d427ab499b4ad6323b844289bcfebf6a"
},
"btc:usd": {
"pubkey": "AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2",
"secret": "233f1f8df9bf0e98c054f4b012eba361ec5050d93f7b06cdb0b2d0242cdf9a2e8cb862071d89cbf390570fa521c0daff5ca0e47f767dc32940801b449e50ad1b"
}
}

View File

@ -5,39 +5,17 @@
"main": "index.js",
"testnetDefaultChannel": "v1.4.8",
"scripts": {
"deploy": "cd src && ts-node cli.ts deploy",
"show:roles": "cd src && ts-node cli.ts roleinfo",
"show:aggregators": "cd src && ts-node cli.ts aggregators",
"show:aggregatorInfo": "cd src && ts-node cli.ts aggregatorInfo",
"show:oracles": "cd src && ts-node cli.ts oracles",
"create:payer": "cd src && ts-node cli.ts create payer",
"create:aggregatorOwner": "cd src && ts-node cli.ts create aggregatorOwner",
"create:oracleOwner": "cd src && ts-node cli.ts create oracleOwner",
"remove:payer": "cd src && ts-node cli.ts remove payer",
"remove:aggregatorOwner": "cd src && ts-node cli.ts remove aggregatorOwner",
"remove:oracleOwner": "cd src && ts-node cli.ts remove oracleOwner",
"airdrop:payer": "cd src && ts-node cli.ts airdrop payer -m 10000000000",
"airdrop:aggregatorOwner": "cd src && ts-node cli.ts airdrop aggregatorOwner",
"airdrop:oracleOwner": "cd src && ts-node cli.ts airdrop oracleOwner",
"add:aggregator": "cd src && ts-node cli.ts add-aggregator",
"add:oracle": "cd src && ts-node cli.ts add-oracle",
"feed": "cd src && ts-node cli.ts feed",
"localnet:update": "solana-localnet update",
"localnet:up": "set -x; solana-localnet down; set -e; solana-localnet up",
"localnet:down": "solana-localnet down",
"clean:roles": "rm -rf src/wallets/*",
"clean:deployed": "rm -rf src/deployed.json",
"clean": "npm run clean:deployed && npm run clean:roles",
"solink": "ts-node src/cli.ts",
"build:program": "solray build program"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@solana/web3.js": "^0.87.1",
"buffer-layout": "^1.2.0",
"commander": "^6.2.0",
"dotenv": "^8.2.0",
"inquirer": "^7.3.3",
"solray": "git+https://github.com/czl1378/solray.git"
"dotenv": "^8.2.0"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",

View File

@ -1,17 +1,21 @@
import {
PublicKey, BaseProgram, Account,
Wallet, System, SPLToken,
import {
PublicKey, BaseProgram, Account,
Wallet, System, SPLToken
} from "solray";
import {
SYSVAR_RENT_PUBKEY, SYSVAR_CLOCK_PUBKEY,
import {
SYSVAR_RENT_PUBKEY, SYSVAR_CLOCK_PUBKEY,
TransactionInstruction, SystemProgram
} from "@solana/web3.js";
import { publicKey, u64LEBuffer, uint64 } from "solray/lib/util/encoding";
import { publicKey, u64LEBuffer, uint64, BufferLayout } from "solray/lib/util/encoding";
import {
decodeOracleInfo
} from "./utils"
// @ts-ignore
import BufferLayout from "buffer-layout";
// import BufferLayout from "buffer-layout";
export const AggregatorLayout = BufferLayout.struct([
BufferLayout.blob(4, "submitInterval"),
@ -109,8 +113,9 @@ export default class FluxAggregator extends BaseProgram {
this.sys = new System(this.wallet);
}
public async initialize(params: InitializeParams): Promise<PublicKey> {
public async initialize(params: InitializeParams): Promise<Account> {
const account = new Account();
await this.sendTx([
await this.sys.createRentFreeAccountInstruction({
newPubicKey: account.publicKey,
@ -123,11 +128,11 @@ export default class FluxAggregator extends BaseProgram {
})
], [this.account, account, params.owner]);
return account.publicKey;
return account;
}
private initializeInstruction(params: InitializeInstructionParams): TransactionInstruction {
const {
let {
aggregator,
description,
submitInterval,
@ -135,7 +140,10 @@ export default class FluxAggregator extends BaseProgram {
maxSubmissionValue,
owner,
} = params;
// FIXME: hmm... should this throw error or what?
description = description.substr(0, 32).toUpperCase().padEnd(32)
const layout = BufferLayout.struct([
BufferLayout.u8("instruction"),
BufferLayout.blob(4, "submitInterval"),
@ -146,7 +154,7 @@ export default class FluxAggregator extends BaseProgram {
const buf = Buffer.allocUnsafe(4);
buf.writeUInt32LE(submitInterval);
return this.instructionEncode(layout, {
instruction: 0, // initialize instruction
submitInterval: buf,
@ -162,7 +170,7 @@ export default class FluxAggregator extends BaseProgram {
public async addOracle(params: AddOracleParams): Promise<PublicKey> {
const account = new Account();
await this.sendTx([
await this.sys.createRentFreeAccountInstruction({
newPubicKey: account.publicKey,
@ -178,9 +186,14 @@ export default class FluxAggregator extends BaseProgram {
return account.publicKey;
}
public async oracleInfo(pubkey: PublicKey) {
const info = await this.conn.getAccountInfo(pubkey)
return decodeOracleInfo(info)
}
private addOracleInstruction(params: AddOracleInstructionParams): TransactionInstruction {
const {
oracle,
oracle,
owner,
description,
aggregator,

View File

@ -1,17 +1,19 @@
import { Command, option } from "commander"
import inquirer from "inquirer"
import fs from "fs"
import path from "path"
import { Connection, PublicKey } from "@solana/web3.js"
import { BPFLoader, PublicKey, Wallet, NetworkName, solana, Deployer } from "solray"
import { BPFLoader, Wallet, NetworkName} from "solray"
import dotenv from "dotenv"
import FluxAggregator, { AggregatorLayout, OracleLayout } from "./FluxAggregator"
import { newWallet, calculatePayfees, connectTo, sleep, decodeAggregatorInfo } from "./utils"
import {
decodeAggregatorInfo,
walletFromEnv,
openDeployer,
} from "./utils"
import * as feed from "./feed"
@ -19,43 +21,68 @@ dotenv.config()
const cli = new Command()
const roles = ["payer", "aggregatorOwner", "oracleOwner"]
const FLUX_AGGREGATOR_SO = path.resolve(__dirname, "../build/flux_aggregator.so")
const network = (process.env.NETWORK || "local") as NetworkName
const conn = solana.connect(network)
const sofilePath = path.resolve(__dirname, "../build/flux_aggregator.so")
class AdminContext {
const deployedPath = path.resolve(__dirname, "./deployed.json")
static readonly AGGREGATOR_PROGRAM = "aggregatorProgram"
const { NETWORK } = process.env
static async load() {
const deployer = await openDeployer()
const admin = await walletFromEnv("ADMIN_MNEMONIC", conn)
const network = (NETWORK || "local") as NetworkName
function checkRole(role) {
if (roles.indexOf(role) < 0) {
error("invalid role")
return new AdminContext(deployer, admin)
}
const walletPath = path.resolve(`./wallets/${role}.json`)
constructor(public deployer: Deployer, public admin: Wallet) {}
return {
exist: fs.existsSync(walletPath),
walletPath
get aggregatorProgram() {
const program = this.deployer.account(AdminContext.AGGREGATOR_PROGRAM)
if (program == null) {
throw new Error(`flux aggregator program is not yet deployed`)
}
return program
}
}
// 30m Black, 31m Red, 32m Green, 33m Yellow, 34m Blue, 35m Magenta, 36m Cyanic, 37m White
function color(s, c="black", b=false): string {
class OracleContext {
static readonly AGGREGATOR_PROGRAM = "aggregatorProgram"
static async load() {
const deployer = await openDeployer()
const wallet = await walletFromEnv("ORACLE_MNEMONIC", conn)
return new OracleContext(deployer, wallet)
}
constructor(public deployer: Deployer, public wallet: Wallet) {}
get aggregatorProgram() {
const program = this.deployer.account(AdminContext.AGGREGATOR_PROGRAM)
if (program == null) {
throw new Error(`flux aggregator program is not yet deployed`)
}
return program
}
}
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"]
let cIdx = cArr.indexOf(c)
let bold = b ? "\x1b[1m" : ""
return `\x1b[${30 + (cIdx > -1 ? cIdx : 0)}m${bold}${s}\x1b[0m`
}
function showNetwork() {
process.stdout.write(`${color(`Network: ${color(network, "blue")}`, "black", true)} \n\n`)
}
function error(message: string) {
console.log("\n")
console.error(color(message, "red"))
@ -67,500 +94,202 @@ function log(message: any) {
console.log(message)
}
async function showRoleInfo(role, conn: Connection): Promise<void> {
const res = checkRole(role)
if (!res) return
if (!res.exist) {
log(`role ${color(role, "red")} not created.`)
return
}
const fileData = fs.readFileSync(res.walletPath)
const wallet = JSON.parse(fileData.toString())
log(color(`[${role}]`, "cyanic", true))
log(`${color("public key: ", "blue")} ${wallet.pubkey}`)
log(`${color("mnemonic: ", "blue")} ${wallet.mnemonic}`)
process.stdout.write(`${color("balance: ", "blue")}...`)
const balance = await conn.getBalance(new PublicKey(wallet.pubkey))
process.stdout.clearLine(-1)
process.stdout.cursorTo(0)
process.stdout.write(`${color("balance: ", "blue")}${balance} \n\n`)
}
cli
.command("create <role>")
.description(`create role account, roles: ${roles.join("|")}`)
.action(async (role) => {
const res = checkRole(role)
if (!res) return
.command("generate-wallet").action(async () => {
const mnemonic = Wallet.generateMnemonic()
const wallet = await Wallet.fromMnemonic(mnemonic, conn)
if (res.exist) {
let fileData = fs.readFileSync(res.walletPath)
let wallet = JSON.parse(fileData.toString())
error(`role ${color(role, "red")} already created, public key: ${color(wallet.pubkey, "blue")}`)
} else {
const wallet = await newWallet()
fs.writeFileSync(res.walletPath, JSON.stringify({
pubkey: wallet.account.publicKey.toBase58(),
secretKey: "["+wallet.account.secretKey.toString()+"]",
mnemonic: wallet.mnemonic,
}))
log(`create role ${color(role, "blue)")} success!`)
}
})
cli
.command("remove <role>")
.description(`remove role account, roles: ${roles.join("|")}`)
.action((role) => {
const res = checkRole(role)
if (!res) return
if (!res.exist) {
error(`role [${role}] not created.`)
}
fs.unlinkSync(res.walletPath)
log(`remove role ${color(role, "blue")} success!`)
})
cli
.command("roleinfo [role]")
.description(`show role info, or all if no role supplied`)
.action(async (role, opts) => {
// show current network
showNetwork()
const conn = await connectTo(network)
if (role) {
showRoleInfo(role, conn)
} else {
for (let i = 0; i < roles.length; i++) {
await showRoleInfo(roles[i], conn)
}
}
log(`address: ${wallet.address}`)
log(`mnemonic: ${mnemonic}`)
})
cli
.command("airdrop <role>")
.description(`request airdrop to the role account, roles: ${roles.join("|")}`)
.option("-m, --amount <amount>", "request amount, default is 10e8", "100000000")
.action(async (role, opts) => {
// show current network
showNetwork()
const res = checkRole(role)
if (!res) return
if (!res.exist) {
error(`role [${role}] not created.`)
}
const fileData = fs.readFileSync(res.walletPath)
const wallet = JSON.parse(fileData.toString())
log(`payer public key: ${color(wallet.pubkey, "blue")}, request airdop...`)
.command("airdrop <address>")
.description(`request airdrop to the address`)
.option("-m, --amount <amount>", "request amount in sol (10e9)", "10")
.action(async (address, opts) => {
const dest = new PublicKey(address)
const { amount } = opts
const conn = await connectTo(network)
await conn.requestAirdrop(new PublicKey(wallet.pubkey), amount*1)
await sleep(1000)
const balance = await conn.getBalance(new PublicKey(wallet.pubkey))
log(`airdop success, balance: ${color(balance, "blue")}`)
log(`requesting 10 sol airdrop to: ${address}`)
await conn.requestAirdrop(dest, amount * 1e9)
log("airdrop success")
})
cli
.command("deploy")
.description("deploy the program")
.action(async (opts) => {
.command("deploy-program")
.description("deploy the aggregator program")
.action(async () => {
const { admin, deployer } = await AdminContext.load()
// show current network
showNetwork()
const programAccount = await deployer.ensure(AdminContext.AGGREGATOR_PROGRAM, async () => {
const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
if (fs.existsSync(deployedPath)) {
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
log(`already deployed, program id: ${color(deployed.programId, "blue")}`)
error("if you want to deployed again, try `npm run clean:deployed`")
}
log(`deploying ${FLUX_AGGREGATOR_SO}...`)
const bpfLoader = new BPFLoader(admin)
const res = checkRole("payer")
if (!res || !res.exist) {
error(`role [payer] not created`)
}
const fileData = fs.readFileSync(res.walletPath)
const payer = JSON.parse(fileData.toString())
return bpfLoader.load(programBinary)
})
if (!fs.existsSync(sofilePath)) {
error("program file not exists")
}
const programBinary = fs.readFileSync(sofilePath)
const conn = await connectTo(network)
const fees = await calculatePayfees(programBinary.length, conn)
let balance = await conn.getBalance(new PublicKey(payer.pubkey))
log(`payer wallet: ${color(payer.pubkey, "blue")}, balance: ${color(balance, "blue")}`)
log(`deploy payfees: ${color(fees, "blue")}`)
if (balance < fees) {
error("insufficient balance to pay fees")
}
log("deploying...")
const wallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
const bpfLoader = new BPFLoader(wallet)
const programAccount = await bpfLoader.load(programBinary)
log(`deploy success, program id: ${color(programAccount.publicKey.toBase58(), "blue")}`)
fs.writeFileSync(deployedPath, JSON.stringify({
network,
programId: programAccount.publicKey.toBase58()
}))
log(`deployed aggregator program. program id: ${color(programAccount.publicKey.toBase58(), "blue")}`)
})
cli
.command("add-aggregator")
.description("add an aggregator")
.action(async () => {
// show current network
showNetwork()
.description("create an aggregator")
.option("--feedName <string>", "feed pair name")
.option("--submitInterval <number>", "min wait time between submissions", "6")
.option("--minSubmissionValue <number>", "minSubmissionValue", "0")
.option("--maxSubmissionValue <number>", "maxSubmissionValue", "18446744073709551615")
.action(async (opts) => {
const { deployer, admin, aggregatorProgram } = await AdminContext.load()
if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet")
}
const { feedName, submitInterval, minSubmissionValue, maxSubmissionValue } = opts
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
const aggregator = new FluxAggregator(admin, aggregatorProgram.publicKey)
if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
}
if (!deployed.programId) {
error("program haven't deployed yet")
}
let res = checkRole("payer")
if (!res || !res.exist) {
error(`role ${color("payer", "blue")} not created`)
}
const payer = JSON.parse(fs.readFileSync(res.walletPath).toString())
res = checkRole("aggregatorOwner")
if (!res || !res.exist) {
error(`role ${color("aggregatorOwner", "blue")} not created, please create the role first`)
}
const aggregatorOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
const inputs = await inquirer
.prompt([
{ message: "Pair name (eg. ETH/USD)", type: "input", name: "pairName", validate: (input) => {
if (!input) {
return "pair name cannot be empty"
}
if (deployed.pairs && deployed.pairs.some((p) => p.pairName == input)) {
return "pair name exist"
}
return true
}, transformer: (input) => {
return input.substr(0, 32).toUpperCase()
} },
{ message: "Submit interval", type: "number", name: "submitInterval", default: 6 },
{ message: "Min submission value", type: "number", name: "minSubmissionValue", default: 100 },
{ message: "Max submission value", type: "number", name: "maxSubmissionValue", default: 10e9 },
])
const { pairName, submitInterval, minSubmissionValue, maxSubmissionValue } = inputs
const conn = await connectTo(network)
const payerWallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
const aggregatorOwnerWallet = await Wallet.fromMnemonic(aggregatorOwner.mnemonic, conn)
const payerWalletBalance = await conn.getBalance(payerWallet.pubkey)
const fees = await calculatePayfees(AggregatorLayout.span, conn)
if (payerWalletBalance < fees) {
error("insufficient balance to pay fees")
}
const program = new FluxAggregator(payerWallet, new PublicKey(deployed.programId))
let description = pairName.substr(0, 32).toUpperCase().padEnd(32)
const aggregator = await program.initialize({
submitInterval: submitInterval as number,
minSubmissionValue: BigInt(minSubmissionValue),
maxSubmissionValue: BigInt(maxSubmissionValue),
description,
owner: aggregatorOwnerWallet.account
const feed = await deployer.ensure(feedName, async () => {
return aggregator.initialize({
submitInterval: parseInt(submitInterval),
minSubmissionValue: BigInt(minSubmissionValue),
maxSubmissionValue: BigInt(maxSubmissionValue),
description: feedName.substr(0, 32).padEnd(32),
owner: admin.account
})
})
log(`aggregator initialized, pubkey: ${color(aggregator.toBase58(), "blue")}, owner: ${color(aggregatorOwner.pubkey, "blue")}`)
fs.writeFileSync(deployedPath, JSON.stringify({
...deployed,
pairs: (deployed.pairs || []).concat([{
pairName: description.trim(),
aggregator: aggregator.toBase58()
}])
}))
log(`feed initialized, pubkey: ${color(feed.publicKey.toBase58(), "blue")}`)
})
cli
.command("aggregators")
.description("show all aggregators")
.action(() => {
// show current network
showNetwork()
// cli
// .command("aggregators")
// .description("show all aggregators")
// .action(() => {
// // show current network
// showNetwork()
if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet")
}
// if (!fs.existsSync(deployedPath)) {
// error("program haven't deployed yet")
// }
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
// const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
}
// if (deployed.network != network) {
// error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
// }
if (!deployed.programId) {
error("program haven't deployed yet")
}
// if (!deployed.programId) {
// error("program haven't deployed yet")
// }
log(deployed.pairs)
})
// log(deployed.pairs)
// })
cli
.command("add-oracle")
.description("add an oracle to aggregator")
.action(async () => {
// show current network
showNetwork()
.option("--feedAddress <string>", "feed address")
.option("--oracleName <string>", "oracle name")
.option("--oracleOwner <string>", "oracle owner address")
.action(async (opts) => {
const { deployer, admin, aggregatorProgram } = await AdminContext.load()
if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet")
}
const { oracleName, oracleOwner, feedAddress } = opts
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
}
if (!deployed.programId) {
error("program haven't deployed yet")
}
if (!deployed.pairs) {
error("no aggregators")
}
let res = checkRole("payer")
if (!res || !res.exist) {
error(`role ${color("payer", "blue")} not created`)
}
const payer = JSON.parse(fs.readFileSync(res.walletPath).toString())
res = checkRole("aggregatorOwner")
if (!res || !res.exist) {
error(`role ${color("aggregatorOwner", "blue")} not created, please create the role first`)
}
const aggregatorOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
res = checkRole("oracleOwner")
if (!res || !res.exist) {
error(`role ${color("oracleOwner", "blue")} not created, please create the role first`)
}
const oracleOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
const inputs = await inquirer
.prompt([
{ message: "Choose an aggregator", type: "list", name: "aggregator", choices: () => {
return deployed.pairs.map(p => ({ name: p.pairName.trim() + ` [${p.aggregator}]`, value: p.aggregator }))
}},
{ message: "Oracle name (eg. Solink)", type: "input", name: "oracleName", validate: (input) => {
if (!input) {
return "oracle name cannot be empty"
}
return true
} }
])
const { oracleName, aggregator } = inputs
const conn = await connectTo(network)
const payerWallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
const aggregatorOwnerWallet = await Wallet.fromMnemonic(aggregatorOwner.mnemonic, conn)
const payerWalletBalance = await conn.getBalance(payerWallet.pubkey)
const fees = await calculatePayfees(OracleLayout.span, conn)
if (payerWalletBalance < fees) {
error("insufficient balance to pay fees")
}
const program = new FluxAggregator(payerWallet, new PublicKey(deployed.programId))
const program = new FluxAggregator(admin, aggregatorProgram.publicKey)
log("add oracle...")
const oracle = await program.addOracle({
owner: new PublicKey(oracleOwner.pubkey),
description: oracleName.substr(0,32).padEnd(32),
aggregator: new PublicKey(aggregator),
aggregatorOwner: aggregatorOwnerWallet.account,
owner: new PublicKey(oracleOwner),
description: oracleName.substr(0, 32).padEnd(32),
aggregator: new PublicKey(feedAddress),
aggregatorOwner: admin.account,
})
log(`add oracle success, pubkey: ${color(oracle.toBase58(), "blue")}, owner: ${color(oracleOwner.pubkey, "blue")}`)
fs.writeFileSync(deployedPath, JSON.stringify({
...deployed,
oracles: (deployed.oracles || []).concat([{
name: oracleName,
aggregator,
pubkey: oracle.toBase58()
}])
}))
log(`added oracle. pubkey: ${color(oracle.toBase58(), "blue")}`)
})
cli
.command("oracles")
.description("show all oracles")
.action(() => {
// show current network
showNetwork()
// cli
// .command("oracles")
// .description("show all oracles")
// .action(() => {
// // show current network
// showNetwork()
if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet")
}
// if (!fs.existsSync(deployedPath)) {
// error("program haven't deployed yet")
// }
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
// const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
}
// if (deployed.network != network) {
// error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
// }
if (!deployed.programId) {
error("program haven't deployed yet")
}
// if (!deployed.programId) {
// error("program haven't deployed yet")
// }
log(deployed.oracles)
})
// log(deployed.oracles)
// })
cli
.command("aggregatorInfo")
.description("show aggregatorInfo")
.action(async () => {
// show current network
showNetwork()
// cli
// .command("aggregatorInfo")
// .description("show aggregatorInfo")
// .action(async () => {
// // show current network
// showNetwork()
if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet")
}
// if (!fs.existsSync(deployedPath)) {
// error("program haven't deployed yet")
// }
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
// const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
}
// if (deployed.network != network) {
// error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
// }
if (!deployed.programId) {
error("program haven't deployed yet")
}
// if (!deployed.programId) {
// error("program haven't deployed yet")
// }
const inputs = await inquirer
.prompt([
{ message: "Choose an aggregator", type: "list", name: "aggregator", choices: () => {
return deployed.pairs.map(p => ({ name: p.pairName.trim() + ` [${p.aggregator}]`, value: p.aggregator }))
}},
])
// const inputs = await inquirer
// .prompt([
// {
// message: "Choose an aggregator", type: "list", name: "aggregator", choices: () => {
// return deployed.pairs.map(p => ({ name: p.pairName.trim() + ` [${p.aggregator}]`, value: p.aggregator }))
// }
// },
// ])
const { aggregator } = inputs
const conn = await connectTo(network)
const accountInfo = await conn.getAccountInfo(new PublicKey(aggregator))
// const { aggregator } = inputs
// const conn = await connectTo(network)
log(decodeAggregatorInfo(accountInfo))
})
// const accountInfo = await conn.getAccountInfo(new PublicKey(aggregator))
// log(decodeAggregatorInfo(accountInfo))
// })
cli
.command("feed")
.description("oracle feeds to aggregator")
.action(async () => {
// show current network
showNetwork()
.option("--feedAddress <string>", "feed address to submit values to")
.option("--oracleAddress <string>", "feed address to submit values to")
.action(async (opts) => {
if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet")
}
const { wallet, aggregatorProgram } = await OracleContext.load()
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
}
if (!deployed.programId) {
error("program haven't deployed yet")
}
const inputs = await inquirer
.prompt([
{ message: "Choose an oracle", type: "list", name: "oracle", choices: () => {
return deployed.oracles.map(p => ({ name: p.name+ ` [${p.pubkey}]`, value: `${p.pubkey}|${p.aggregator}` }))
}},
])
const tmpArr = inputs.oracle.split("|")
let res = checkRole("payer")
if (!res || !res.exist) {
error(`role ${color("payer", "blue")} not created`)
}
const payer = JSON.parse(fs.readFileSync(res.walletPath).toString())
res = checkRole("oracleOwner")
if (!res || !res.exist) {
error(`role ${color("oracleOwner", "blue")} not created, please create the role first`)
}
const oracleOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
let oracle = tmpArr[0], aggregator = tmpArr[1]
let pair = ""
deployed.pairs.map((p) => {
if (p.aggregator == aggregator) {
pair = p.pairName
}
})
const conn = await connectTo(network)
const payerWallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
const oracleOwnerWallet = await Wallet.fromMnemonic(oracleOwner.mnemonic, conn)
const { feedAddress, oracleAddress } = opts
feed.start({
oracle: new PublicKey(oracle),
oracleOwner: oracleOwnerWallet.account,
aggregator: new PublicKey(aggregator),
pair,
payerWallet,
programId: new PublicKey(deployed.programId)
oracle: new PublicKey(oracleAddress),
oracleOwner: wallet.account,
feed: new PublicKey(feedAddress),
pairSymbol: "BTC-USD",
payerWallet: wallet,
programId: aggregatorProgram.publicKey,
})
})

View File

@ -1 +0,0 @@
{"network":"local","programId":"95mRqquh7vGi31i87g2tnozV9ngap9GtnkpsHp31kFt1","pairs":[{"pairName":"ETH/USD","aggregator":"GNUKT4Ug3574s6tuWXtPhC9u1VPWgx4XXVCcD8usXHGW"}],"oracles":[{"name":"Solink","aggregator":"GNUKT4Ug3574s6tuWXtPhC9u1VPWgx4XXVCcD8usXHGW","pubkey":"5fs1WPxopQfgRWujfShfh6qLuPGkHjyMYbgxtzZyNtrp"}]}

View File

@ -1,44 +1,40 @@
import { PublicKey, Account, Wallet } from "solray"
import WebSocket from "ws"
import { decodeOracleInfo } from "./utils"
import { decodeOracleInfo, sleep } from "./utils"
import FluxAggregator from "./FluxAggregator"
let nextSubmitTime = new Date().getTime()
let submiting = false
const submitInterval = 10 * 1000
interface StartParams {
oracle: PublicKey;
oracleOwner: Account;
aggregator: PublicKey;
pair: string;
feed: PublicKey;
pairSymbol: string;
payerWallet: Wallet;
programId: PublicKey;
}
export async function start(params: StartParams) {
const {
oracle,
oracleOwner,
aggregator,
pair,
oracle,
oracleOwner,
feed,
pairSymbol,
payerWallet,
programId,
} = params
console.log("ready to feeds...")
console.log("connecting to wss://ws-feed.pro.coinbase.com ()")
const ws = new WebSocket("wss://ws-feed.pro.coinbase.com")
const program = new FluxAggregator(payerWallet, programId)
ws.on("open", () => {
console.log(`${pair} price feed connected`)
console.log(`${pairSymbol} price feed connected`)
ws.send(JSON.stringify({
"type": "subscribe",
"product_ids": [
pair.replace("/", "-").toUpperCase(),
pairSymbol.replace("/", "-").toUpperCase(),
],
"channels": [
"ticker"
@ -46,45 +42,49 @@ export async function start(params: StartParams) {
}))
})
ws.on("message", (data) => {
// in penny
let curPriceCent = 0
ws.on("message", async (data) => {
const json = JSON.parse(data)
if (!json || !json.price) {
return console.log(data)
}
if (submiting) return false
console.log("new price:", json.price)
let now = new Date().getTime()
if (now < nextSubmitTime) {
console.log("submit cooling...")
return false
curPriceCent = Math.floor(json.price * 100)
console.log("current price:", json.price)
})
ws.on("close", (err) => {
console.error(`websocket closed: ${err}`)
process.exit(1)
})
const program = new FluxAggregator(payerWallet, programId)
console.log(await program.oracleInfo(oracle))
console.log({ owner: oracleOwner.publicKey.toString() })
while (true) {
if (curPriceCent == 0) {
await sleep(1000)
}
submiting = true
program.submit({
aggregator,
await program.submit({
aggregator: feed,
oracle,
submission: BigInt(parseInt((json.price * 100) as any)),
submission: BigInt(curPriceCent),
owner: oracleOwner,
}).then(() => {
console.log("submit success!")
nextSubmitTime = now + submitInterval
payerWallet.conn.getAccountInfo(oracle).then((accountInfo) => {
console.log("oracle info:", decodeOracleInfo(accountInfo))
})
}).catch((err) => {
console.log(err)
}).finally(() => {
submiting = false
})
})
ws.on("close", (error) => {
console.error(error)
})
console.log("submit success!")
payerWallet.conn.getAccountInfo(oracle).then((accountInfo) => {
console.log("oracle info:", decodeOracleInfo(accountInfo))
})
console.log("wait for cooldown success!")
await sleep(submitInterval)
}
}

View File

@ -1,25 +1,8 @@
import { Connection, BpfLoader, PublicKey } from "@solana/web3.js"
import { Connection, PublicKey } from "@solana/web3.js"
import { AggregatorLayout, SubmissionLayout, OracleLayout } from "./FluxAggregator"
import { solana, Wallet, NetworkName } from "solray"
export async function calculatePayfees(dataLength: number, conn: Connection): Promise<number> {
let fees = 0
const { feeCalculator } = await conn.getRecentBlockhash()
const NUM_RETRIES = 500
fees +=
feeCalculator.lamportsPerSignature *
(BpfLoader.getMinNumSignatures(dataLength) + NUM_RETRIES) +
(await conn.getMinimumBalanceForRentExemption(dataLength))
// Calculate the cost of sending the transactions
fees += feeCalculator.lamportsPerSignature * 100
return fees
}
import { solana, Wallet, NetworkName, Deployer } from "solray"
export function getSubmissionValue(submissions: []): number {
const values = submissions
@ -56,7 +39,7 @@ export function decodeAggregatorInfo(accountInfo) {
const maxSubmissionValue = aggregator.maxSubmissionValue.readBigUInt64LE().toString()
const submitInterval = aggregator.submitInterval.readInt32LE()
const description = aggregator.description.toString()
// decode oracles
let submissions: [] = []
let oracles: [] = []
@ -68,13 +51,13 @@ export function decodeAggregatorInfo(accountInfo) {
aggregator.submissions.slice(i*submissionSpace, (i+1)*submissionSpace)
)
submission.oracle = new PublicKey(submission.oracle)
submission.time = submission.time.readBigInt64LE().toString()
submission.value = submission.value.readBigInt64LE().toString()*1
if (!submission.oracle.equals(new PublicKey(0))) {
submissions.push(submission as never)
oracles.push(submission.oracle.toBase58() as never)
}
if (submission.time > updateTime) {
updateTime = submission.time
@ -94,7 +77,7 @@ export function decodeAggregatorInfo(accountInfo) {
export function decodeOracleInfo(accountInfo) {
const data = Buffer.from(accountInfo.data)
const oracle = OracleLayout.decode(data)
oracle.submission = oracle.submission.readBigUInt64LE().toString()
@ -108,18 +91,21 @@ export function decodeOracleInfo(accountInfo) {
return oracle
}
export async function connectTo(network: NetworkName): Promise<Connection> {
const conn = solana.connect(network as NetworkName)
return conn
}
export async function newWallet(): Promise<any> {
const mnemonic = Wallet.generateMnemonic()
const wallet = await Wallet.fromMnemonic(mnemonic, null as any)
return {
mnemonic,
account: wallet.account
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`)
}
return Wallet.fromMnemonic(mnemonic, conn)
}
export async function openDeployer(): Promise<Deployer> {
const deployFile = process.env.DEPLOY_FILE
if (!deployFile) {
throw new Error(`Set DEPLOY_FILE in .env`)
}
return Deployer.open(deployFile)
}

View File

@ -1 +0,0 @@
{"pubkey":"2ATirbVBRtEGGY3jeEcEsTGLBVSJEvk6WLb1K7droYp2","secretKey":"[183,247,179,232,164,36,17,253,149,205,222,132,4,4,154,4,206,110,37,117,129,66,18,146,95,45,172,29,62,133,203,242,17,72,32,226,225,18,116,158,33,176,7,145,15,65,108,116,53,15,28,112,25,66,150,7,240,20,112,110,21,92,250,67]","mnemonic":"forget mushroom capable trim chapter rally long congress humor maximum title citizen"}

View File

@ -1 +0,0 @@
{"pubkey":"FUZDeYACNRvDYD9A1cmrEqfZ5GiE5rBEfc2rcXW2sXxw","secretKey":"[18,53,121,118,64,233,227,172,4,14,12,103,202,93,26,114,190,103,143,81,125,144,144,156,74,251,138,251,131,128,135,13,215,18,186,86,171,14,169,75,62,159,168,103,242,239,62,120,134,113,144,77,53,136,194,105,57,172,193,138,105,231,244,228]","mnemonic":"subway wine design chuckle tooth helmet solution butter tooth slab leopard useful"}

View File

@ -1 +0,0 @@
{"pubkey":"79AnPHtN7P4e36CDrP9CzuVRbwRUZdVMQ7weFiktYjPv","secretKey":"[224,93,108,163,202,117,226,167,224,216,219,4,51,192,130,204,111,22,71,199,97,113,139,183,236,194,229,135,214,12,231,108,91,61,212,65,148,88,37,162,244,108,41,171,74,32,204,29,135,215,103,88,134,32,115,81,222,40,175,222,205,208,152,65]","mnemonic":"dove ribbon nut beef trouble language unfair quiz sand wash copper result"}

1411
yarn.lock Normal file

File diff suppressed because it is too large Load Diff