refactor cli
This commit is contained in:
parent
7749dff9ea
commit
faa0f8e8c6
5
.env
5
.env
|
@ -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"
|
92
README.md
92
README.md
|
@ -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
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"aggregatorProgram": {
|
||||||
|
"pubkey": "9KXbVqUrMgtti7Jx4rrV1NqXjQNxWaKgtYCEwJ8AESS5",
|
||||||
|
"secret": "a2b92fa9514ac18a40c2bc895db0f97cabc1870205a74d84a0fb4a8167e6cba37b9c8b296cb261a7f3b17b294c72c4b6d427ab499b4ad6323b844289bcfebf6a"
|
||||||
|
},
|
||||||
|
"btc:usd": {
|
||||||
|
"pubkey": "AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2",
|
||||||
|
"secret": "233f1f8df9bf0e98c054f4b012eba361ec5050d93f7b06cdb0b2d0242cdf9a2e8cb862071d89cbf390570fa521c0daff5ca0e47f767dc32940801b449e50ad1b"
|
||||||
|
}
|
||||||
|
}
|
30
package.json
30
package.json
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
635
src/cli.ts
635
src/cli.ts
|
@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{"network":"local","programId":"95mRqquh7vGi31i87g2tnozV9ngap9GtnkpsHp31kFt1","pairs":[{"pairName":"ETH/USD","aggregator":"GNUKT4Ug3574s6tuWXtPhC9u1VPWgx4XXVCcD8usXHGW"}],"oracles":[{"name":"Solink","aggregator":"GNUKT4Ug3574s6tuWXtPhC9u1VPWgx4XXVCcD8usXHGW","pubkey":"5fs1WPxopQfgRWujfShfh6qLuPGkHjyMYbgxtzZyNtrp"}]}
|
|
88
src/feed.ts
88
src/feed.ts
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
58
src/utils.ts
58
src/utils.ts
|
@ -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)
|
||||||
|
}
|
|
@ -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"}
|
|
|
@ -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"}
|
|
|
@ -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"}
|
|
Loading…
Reference in New Issue