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 # solana-flux-aggregator
Solnana Flux Aggregator Solnana Flux Aggregator
## Install ## 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", "main": "index.js",
"testnetDefaultChannel": "v1.4.8", "testnetDefaultChannel": "v1.4.8",
"scripts": { "scripts": {
"deploy": "cd src && ts-node cli.ts deploy", "solink": "ts-node src/cli.ts",
"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",
"build:program": "solray build program" "build:program": "solray build program"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@solana/web3.js": "^0.87.1",
"buffer-layout": "^1.2.0",
"commander": "^6.2.0", "commander": "^6.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0"
"inquirer": "^7.3.3",
"solray": "git+https://github.com/czl1378/solray.git"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/recommended": "^1.0.1", "@tsconfig/recommended": "^1.0.1",

View File

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

View File

@ -1,17 +1,19 @@
import { Command, option } from "commander" import { Command, option } from "commander"
import inquirer from "inquirer"
import fs from "fs" import fs from "fs"
import path from "path" 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 dotenv from "dotenv"
import FluxAggregator, { AggregatorLayout, OracleLayout } from "./FluxAggregator" 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" import * as feed from "./feed"
@ -19,43 +21,68 @@ dotenv.config()
const cli = new Command() 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 return new AdminContext(deployer, admin)
function checkRole(role) {
if (roles.indexOf(role) < 0) {
error("invalid role")
} }
const walletPath = path.resolve(`./wallets/${role}.json`) constructor(public deployer: Deployer, public admin: Wallet) {}
return { get aggregatorProgram() {
exist: fs.existsSync(walletPath), const program = this.deployer.account(AdminContext.AGGREGATOR_PROGRAM)
walletPath
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 class OracleContext {
function color(s, c="black", b=false): string {
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"] 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" : ""
return `\x1b[${30 + (cIdx > -1 ? cIdx : 0)}m${bold}${s}\x1b[0m` 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) { function error(message: string) {
console.log("\n") console.log("\n")
console.error(color(message, "red")) console.error(color(message, "red"))
@ -67,500 +94,202 @@ function log(message: any) {
console.log(message) 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 cli
.command("create <role>") .command("generate-wallet").action(async () => {
.description(`create role account, roles: ${roles.join("|")}`) const mnemonic = Wallet.generateMnemonic()
.action(async (role) => { const wallet = await Wallet.fromMnemonic(mnemonic, conn)
const res = checkRole(role)
if (!res) return
if (res.exist) { log(`address: ${wallet.address}`)
let fileData = fs.readFileSync(res.walletPath) log(`mnemonic: ${mnemonic}`)
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)
}
}
}) })
cli cli
.command("airdrop <role>") .command("airdrop <address>")
.description(`request airdrop to the role account, roles: ${roles.join("|")}`) .description(`request airdrop to the address`)
.option("-m, --amount <amount>", "request amount, default is 10e8", "100000000") .option("-m, --amount <amount>", "request amount in sol (10e9)", "10")
.action(async (role, opts) => { .action(async (address, opts) => {
const dest = new PublicKey(address)
// 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...`)
const { amount } = opts const { amount } = opts
const conn = await connectTo(network)
await conn.requestAirdrop(new PublicKey(wallet.pubkey), amount*1) log(`requesting 10 sol airdrop to: ${address}`)
await sleep(1000) await conn.requestAirdrop(dest, amount * 1e9)
const balance = await conn.getBalance(new PublicKey(wallet.pubkey)) log("airdrop success")
log(`airdop success, balance: ${color(balance, "blue")}`)
}) })
cli cli
.command("deploy") .command("deploy-program")
.description("deploy the program") .description("deploy the aggregator program")
.action(async (opts) => { .action(async () => {
const { admin, deployer } = await AdminContext.load()
// show current network const programAccount = await deployer.ensure(AdminContext.AGGREGATOR_PROGRAM, async () => {
showNetwork() const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO)
if (fs.existsSync(deployedPath)) { log(`deploying ${FLUX_AGGREGATOR_SO}...`)
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString()) const bpfLoader = new BPFLoader(admin)
log(`already deployed, program id: ${color(deployed.programId, "blue")}`)
error("if you want to deployed again, try `npm run clean:deployed`")
}
const res = checkRole("payer") return bpfLoader.load(programBinary)
if (!res || !res.exist) { })
error(`role [payer] not created`)
}
const fileData = fs.readFileSync(res.walletPath)
const payer = JSON.parse(fileData.toString())
if (!fs.existsSync(sofilePath)) { log(`deployed aggregator program. program id: ${color(programAccount.publicKey.toBase58(), "blue")}`)
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()
}))
}) })
cli cli
.command("add-aggregator") .command("add-aggregator")
.description("add an aggregator") .description("create an aggregator")
.action(async () => { .option("--feedName <string>", "feed pair name")
// show current network .option("--submitInterval <number>", "min wait time between submissions", "6")
showNetwork() .option("--minSubmissionValue <number>", "minSubmissionValue", "0")
.option("--maxSubmissionValue <number>", "maxSubmissionValue", "18446744073709551615")
.action(async (opts) => {
const { deployer, admin, aggregatorProgram } = await AdminContext.load()
if (!fs.existsSync(deployedPath)) { const { feedName, submitInterval, minSubmissionValue, maxSubmissionValue } = opts
error("program haven't deployed yet")
}
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString()) const aggregator = new FluxAggregator(admin, aggregatorProgram.publicKey)
if (deployed.network != network) { const feed = await deployer.ensure(feedName, async () => {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again") return aggregator.initialize({
} submitInterval: parseInt(submitInterval),
minSubmissionValue: BigInt(minSubmissionValue),
if (!deployed.programId) { maxSubmissionValue: BigInt(maxSubmissionValue),
error("program haven't deployed yet") description: feedName.substr(0, 32).padEnd(32),
} owner: admin.account
})
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
}) })
log(`aggregator initialized, pubkey: ${color(aggregator.toBase58(), "blue")}, owner: ${color(aggregatorOwner.pubkey, "blue")}`) log(`feed initialized, pubkey: ${color(feed.publicKey.toBase58(), "blue")}`)
fs.writeFileSync(deployedPath, JSON.stringify({
...deployed,
pairs: (deployed.pairs || []).concat([{
pairName: description.trim(),
aggregator: aggregator.toBase58()
}])
}))
}) })
cli // cli
.command("aggregators") // .command("aggregators")
.description("show all aggregators") // .description("show all aggregators")
.action(() => { // .action(() => {
// show current network // // show current network
showNetwork() // showNetwork()
if (!fs.existsSync(deployedPath)) { // if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet") // 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) { // if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again") // error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
} // }
if (!deployed.programId) { // if (!deployed.programId) {
error("program haven't deployed yet") // error("program haven't deployed yet")
} // }
log(deployed.pairs) // log(deployed.pairs)
}) // })
cli cli
.command("add-oracle") .command("add-oracle")
.description("add an oracle to aggregator") .description("add an oracle to aggregator")
.action(async () => { .option("--feedAddress <string>", "feed address")
// show current network .option("--oracleName <string>", "oracle name")
showNetwork() .option("--oracleOwner <string>", "oracle owner address")
.action(async (opts) => {
const { deployer, admin, aggregatorProgram } = await AdminContext.load()
if (!fs.existsSync(deployedPath)) { const { oracleName, oracleOwner, feedAddress } = opts
error("program haven't deployed yet")
}
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString()) const program = 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")
}
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))
log("add oracle...") log("add oracle...")
const oracle = await program.addOracle({ const oracle = await program.addOracle({
owner: new PublicKey(oracleOwner.pubkey), owner: new PublicKey(oracleOwner),
description: oracleName.substr(0,32).padEnd(32), description: oracleName.substr(0, 32).padEnd(32),
aggregator: new PublicKey(aggregator), aggregator: new PublicKey(feedAddress),
aggregatorOwner: aggregatorOwnerWallet.account, aggregatorOwner: admin.account,
}) })
log(`add oracle success, pubkey: ${color(oracle.toBase58(), "blue")}, owner: ${color(oracleOwner.pubkey, "blue")}`) log(`added oracle. pubkey: ${color(oracle.toBase58(), "blue")}`)
fs.writeFileSync(deployedPath, JSON.stringify({
...deployed,
oracles: (deployed.oracles || []).concat([{
name: oracleName,
aggregator,
pubkey: oracle.toBase58()
}])
}))
}) })
cli // cli
.command("oracles") // .command("oracles")
.description("show all oracles") // .description("show all oracles")
.action(() => { // .action(() => {
// show current network // // show current network
showNetwork() // showNetwork()
if (!fs.existsSync(deployedPath)) { // if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet") // 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) { // if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again") // error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
} // }
if (!deployed.programId) { // if (!deployed.programId) {
error("program haven't deployed yet") // error("program haven't deployed yet")
} // }
log(deployed.oracles) // log(deployed.oracles)
}) // })
cli // cli
.command("aggregatorInfo") // .command("aggregatorInfo")
.description("show aggregatorInfo") // .description("show aggregatorInfo")
.action(async () => { // .action(async () => {
// show current network // // show current network
showNetwork() // showNetwork()
if (!fs.existsSync(deployedPath)) { // if (!fs.existsSync(deployedPath)) {
error("program haven't deployed yet") // 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) { // if (deployed.network != network) {
error("deployed network not match, please try `npm run clean:deployed`, and deploy again") // error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
} // }
if (!deployed.programId) { // if (!deployed.programId) {
error("program haven't deployed yet") // error("program haven't deployed yet")
} // }
const inputs = await inquirer // const inputs = await inquirer
.prompt([ // .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: "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 { aggregator } = inputs
const conn = await connectTo(network) // const conn = await connectTo(network)
const accountInfo = await conn.getAccountInfo(new PublicKey(aggregator))
log(decodeAggregatorInfo(accountInfo)) // const accountInfo = await conn.getAccountInfo(new PublicKey(aggregator))
})
// log(decodeAggregatorInfo(accountInfo))
// })
cli cli
.command("feed") .command("feed")
.description("oracle feeds to aggregator") .description("oracle feeds to aggregator")
.action(async () => { .option("--feedAddress <string>", "feed address to submit values to")
// show current network .option("--oracleAddress <string>", "feed address to submit values to")
showNetwork() .action(async (opts) => {
if (!fs.existsSync(deployedPath)) { const { wallet, aggregatorProgram } = await OracleContext.load()
error("program haven't deployed yet")
}
const deployed = JSON.parse(fs.readFileSync(deployedPath).toString()) const { feedAddress, oracleAddress } = opts
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)
feed.start({ feed.start({
oracle: new PublicKey(oracle), oracle: new PublicKey(oracleAddress),
oracleOwner: oracleOwnerWallet.account, oracleOwner: wallet.account,
aggregator: new PublicKey(aggregator), feed: new PublicKey(feedAddress),
pair, pairSymbol: "BTC-USD",
payerWallet, payerWallet: wallet,
programId: new PublicKey(deployed.programId) 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 { PublicKey, Account, Wallet } from "solray"
import WebSocket from "ws" import WebSocket from "ws"
import { decodeOracleInfo } from "./utils" import { decodeOracleInfo, sleep } from "./utils"
import FluxAggregator from "./FluxAggregator" import FluxAggregator from "./FluxAggregator"
let nextSubmitTime = new Date().getTime()
let submiting = false
const submitInterval = 10 * 1000 const submitInterval = 10 * 1000
interface StartParams { interface StartParams {
oracle: PublicKey; oracle: PublicKey;
oracleOwner: Account; oracleOwner: Account;
aggregator: PublicKey; feed: PublicKey;
pair: string; pairSymbol: string;
payerWallet: Wallet; payerWallet: Wallet;
programId: PublicKey; programId: PublicKey;
} }
export async function start(params: StartParams) { export async function start(params: StartParams) {
const { const {
oracle, oracle,
oracleOwner, oracleOwner,
aggregator, feed,
pair, pairSymbol,
payerWallet, payerWallet,
programId, programId,
} = params } = 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 ws = new WebSocket("wss://ws-feed.pro.coinbase.com")
const program = new FluxAggregator(payerWallet, programId)
ws.on("open", () => { ws.on("open", () => {
console.log(`${pair} price feed connected`) console.log(`${pairSymbol} price feed connected`)
ws.send(JSON.stringify({ ws.send(JSON.stringify({
"type": "subscribe", "type": "subscribe",
"product_ids": [ "product_ids": [
pair.replace("/", "-").toUpperCase(), pairSymbol.replace("/", "-").toUpperCase(),
], ],
"channels": [ "channels": [
"ticker" "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) const json = JSON.parse(data)
if (!json || !json.price) { if (!json || !json.price) {
return console.log(data) return console.log(data)
} }
if (submiting) return false
console.log("new price:", json.price) curPriceCent = Math.floor(json.price * 100)
let now = new Date().getTime()
if (now < nextSubmitTime) { console.log("current price:", json.price)
console.log("submit cooling...") })
return false
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 await program.submit({
aggregator: feed,
program.submit({
aggregator,
oracle, oracle,
submission: BigInt(parseInt((json.price * 100) as any)), submission: BigInt(curPriceCent),
owner: oracleOwner, 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.log("submit success!")
console.error(error)
}) 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 { AggregatorLayout, SubmissionLayout, OracleLayout } from "./FluxAggregator"
import { solana, Wallet, NetworkName } from "solray" import { solana, Wallet, NetworkName, Deployer } 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
}
export function getSubmissionValue(submissions: []): number { export function getSubmissionValue(submissions: []): number {
const values = submissions const values = submissions
@ -56,7 +39,7 @@ export function decodeAggregatorInfo(accountInfo) {
const maxSubmissionValue = aggregator.maxSubmissionValue.readBigUInt64LE().toString() const maxSubmissionValue = aggregator.maxSubmissionValue.readBigUInt64LE().toString()
const submitInterval = aggregator.submitInterval.readInt32LE() const submitInterval = aggregator.submitInterval.readInt32LE()
const description = aggregator.description.toString() const description = aggregator.description.toString()
// decode oracles // decode oracles
let submissions: [] = [] let submissions: [] = []
let oracles: [] = [] let oracles: [] = []
@ -68,13 +51,13 @@ export function decodeAggregatorInfo(accountInfo) {
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)
submission.time = submission.time.readBigInt64LE().toString() submission.time = submission.time.readBigInt64LE().toString()
submission.value = submission.value.readBigInt64LE().toString()*1 submission.value = submission.value.readBigInt64LE().toString()*1
if (!submission.oracle.equals(new PublicKey(0))) { if (!submission.oracle.equals(new PublicKey(0))) {
submissions.push(submission as never) submissions.push(submission as never)
oracles.push(submission.oracle.toBase58() as never) oracles.push(submission.oracle.toBase58() as never)
} }
if (submission.time > updateTime) { if (submission.time > updateTime) {
updateTime = submission.time updateTime = submission.time
@ -94,7 +77,7 @@ export function decodeAggregatorInfo(accountInfo) {
export function decodeOracleInfo(accountInfo) { export function decodeOracleInfo(accountInfo) {
const data = Buffer.from(accountInfo.data) const data = Buffer.from(accountInfo.data)
const oracle = OracleLayout.decode(data) const oracle = OracleLayout.decode(data)
oracle.submission = oracle.submission.readBigUInt64LE().toString() oracle.submission = oracle.submission.readBigUInt64LE().toString()
@ -108,18 +91,21 @@ export function decodeOracleInfo(accountInfo) {
return oracle return oracle
} }
export async function connectTo(network: NetworkName): Promise<Connection> { export async function walletFromEnv(key: string, conn: Connection): Promise<Wallet> {
const conn = solana.connect(network as NetworkName) const mnemonic = process.env[key]
return conn if (!mnemonic) {
} throw new Error(`Set ${key} in .env to be a mnemonic`)
export async function newWallet(): Promise<any> {
const mnemonic = Wallet.generateMnemonic()
const wallet = await Wallet.fromMnemonic(mnemonic, null as any)
return {
mnemonic,
account: wallet.account
} }
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