diff --git a/README.md b/README.md
index 83386de..6befc3b 100644
--- a/README.md
+++ b/README.md
@@ -19,18 +19,26 @@ request some airdrop:
`npm run deploy`
-## Create Aggregator
+## Create Aggregator Owner
-`npm run create:aggregator`
+`npm run create:aggregatorOwner`
-request some airdrop:
+## Add An Aggregator
-`npm run airdrop:aggregator`
+`npm run add:aggregator`
-## Create Oracle
+## Create Oracle Owner
-`npm run create:oracle`
+`npm run create:oracleOwner`
-request some airdrop:
+## Add An Oracle
-`npm run airdrop:oracle`
\ No newline at end of file
+`npm run add:oracle`
+
+## Show Aggregators/Oracles
+
+`npm run show:aggregators|oracles`
+
+## Start to feed
+
+`npm run feed`
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6b4f240
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
Google
\ No newline at end of file
diff --git a/package.json b/package.json
index 717a80d..f7ff661 100644
--- a/package.json
+++ b/package.json
@@ -6,20 +6,28 @@
"testnetDefaultChannel": "v1.4.8",
"scripts": {
"deploy": "cd src && ts-node cli.ts deploy",
- "role-info": "cd src && ts-node cli.ts role-info",
+ "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:aggregator": "cd src && ts-node cli.ts create aggregator",
- "create:oracle": "cd src && ts-node cli.ts create oracle",
+ "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:aggregator": "cd src && ts-node cli.ts remove aggregator",
- "remove:oracle": "cd src && ts-node cli.ts remove oracle",
- "airdrop:payer": "cd src && ts-node cli.ts airdrop payer",
- "airdrop:aggregator": "cd src && ts-node cli.ts airdrop aggregator",
- "airdrop:oracle": "cd src && ts-node cli.ts airdrop oracle",
+ "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": "rm -rf src/deployed.md && rm -rf src/wallets/*",
+ "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"
},
"keywords": [],
@@ -28,6 +36,7 @@
"dependencies": {
"commander": "^6.2.0",
"dotenv": "^8.2.0",
+ "inquirer": "^7.3.3",
"solray": "git+https://github.com/czl1378/solray.git"
},
"devDependencies": {
diff --git a/src/.env b/src/.env
new file mode 100644
index 0000000..9df51e7
--- /dev/null
+++ b/src/.env
@@ -0,0 +1 @@
+NETWORK=local
\ No newline at end of file
diff --git a/src/cli.ts b/src/cli.ts
index e6bf562..27b977c 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,27 +1,37 @@
import { Command, option } from "commander"
-import { PublicKey, Connection } from "@solana/web3.js"
-
-import { BPFLoader, Wallet } from "solray"
-
-import {
- newWallet, calculatePayfees, connectTo, sleep
-} from "./utils"
+import inquirer from "inquirer"
import fs from "fs"
import path from "path"
+import { Connection, PublicKey } from "@solana/web3.js"
+
+import { BPFLoader, Wallet, NetworkName} from "solray"
+import dotenv from "dotenv"
+
+import FluxAggregator, { AggregatorLayout, OracleLayout } from "./FluxAggregator"
+
+import { newWallet, calculatePayfees, connectTo, sleep, decodeAggregatorInfo } from "./utils"
+
+import * as feed from "./feed"
+
+dotenv.config()
+
const cli = new Command()
-const roles = ["payer", "aggregator", "oracle"]
+const roles = ["payer", "aggregatorOwner", "oracleOwner"]
const sofilePath = path.resolve(__dirname, "../build/flux_aggregator.so")
-const deployedPath = path.resolve(__dirname, "./deployed.md")
+const deployedPath = path.resolve(__dirname, "./deployed.json")
+
+const { NETWORK } = process.env
+
+const network = (NETWORK || "local") as NetworkName
function checkRole(role) {
if (roles.indexOf(role) < 0) {
- console.error("invalid role")
- return false
+ error("invalid role")
}
const walletPath = path.resolve(`./wallets/${role}.json`)
@@ -42,24 +52,40 @@ function color(s, c="black", b=false): string {
return `\x1b[${30 + (cIdx > -1 ? cIdx : 0)}m${bold}${s}\x1b[0m`
}
+function showNetwork() {
+ process.stdout.write(`${color(`Network: ${color(network, "blue")}`, "black", true)} \n\n`)
+}
+
+function error(message: string) {
+ console.log("\n")
+ console.error(color(message, "red"))
+ console.log("\n")
+ process.exit()
+}
+
+function log(message: any) {
+ console.log(message)
+}
+
async function showRoleInfo(role, conn: Connection): Promise {
const res = checkRole(role)
if (!res) return
if (!res.exist) {
- return console.error(`role ${color(role, "red")} not created.`)
+ log(`role ${color(role, "red")} not created.`)
+ return
}
const fileData = fs.readFileSync(res.walletPath)
const wallet = JSON.parse(fileData.toString())
- console.log(color(`[${role}]`, "cyanic", true))
- console.log(color("public key: ", "blue"), `${wallet.publicKey}`)
- console.log(color("mnemonic: ", "blue"), `${wallet.mnemonic}`)
+ 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.publicKey))
+ 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`)
@@ -76,17 +102,16 @@ cli
if (res.exist) {
let fileData = fs.readFileSync(res.walletPath)
let wallet = JSON.parse(fileData.toString())
- console.error(`role ${color(role, "red")} already created, public key: ${color(wallet.publicKey, "blue")}`)
- return
+ error(`role ${color(role, "red")} already created, public key: ${color(wallet.pubkey, "blue")}`)
} else {
const wallet = await newWallet()
fs.writeFileSync(res.walletPath, JSON.stringify({
- publicKey: wallet.account.publicKey.toBase58(),
+ pubkey: wallet.account.publicKey.toBase58(),
secretKey: "["+wallet.account.secretKey.toString()+"]",
mnemonic: wallet.mnemonic,
}))
- console.log(`create role ${color(role, "blue)")} success!`)
+ log(`create role ${color(role, "blue)")} success!`)
}
})
@@ -100,24 +125,21 @@ cli
if (!res) return
if (!res.exist) {
- return console.error(`role ${color(role, "red")} not created.`)
+ error(`role [${role}] not created.`)
}
fs.unlinkSync(res.walletPath)
- console.log(`remove role ${color(role, "blue")} success!`)
+ log(`remove role ${color(role, "blue")} success!`)
})
cli
- .command("role-info [role]")
+ .command("roleinfo [role]")
.description(`show role info, or all if no role supplied`)
- .option(
- "-n, --network ",
- "deploy on which network (local|devnet|mainnet), default is localnet",
- "local"
- )
.action(async (role, opts) => {
- const { network } = opts
+
+ // show current network
+ showNetwork()
const conn = await connectTo(network)
if (role) {
@@ -132,63 +154,58 @@ cli
cli
.command("airdrop ")
.description(`request airdrop to the role account, roles: ${roles.join("|")}`)
- .option(
- "-n, --network ",
- "deploy on which network (local|devnet|mainnet), default is localnet",
- "local"
- )
.option("-m, --amount ", "request amount, default is 10e8", "100000000")
.action(async (role, opts) => {
+
+ // show current network
+ showNetwork()
+
const res = checkRole(role)
if (!res) return
if (!res.exist) {
- return console.error(`role ${color(role, "red")} not created.`)
+ error(`role [${role}] not created.`)
}
const fileData = fs.readFileSync(res.walletPath)
const wallet = JSON.parse(fileData.toString())
- console.log(`payer public key: ${color(wallet.publicKey, "blue")}, request airdop...`)
+ log(`payer public key: ${color(wallet.pubkey, "blue")}, request airdop...`)
- const { network, amount } = opts
+ const { amount } = opts
const conn = await connectTo(network)
- await conn.requestAirdrop(new PublicKey(wallet.publicKey), amount*1)
+ await conn.requestAirdrop(new PublicKey(wallet.pubkey), amount*1)
await sleep(1000)
- const balance = await conn.getBalance(new PublicKey(wallet.publicKey))
+ const balance = await conn.getBalance(new PublicKey(wallet.pubkey))
- console.log(`airdop success, balance: ${color(balance, "blue")}`)
+ log(`airdop success, balance: ${color(balance, "blue")}`)
})
cli
.command("deploy")
- .description("deploy the aggregator program")
- .option(
- "-n,--network ",
- "deploy on which network (local|devnet|mainnet), default is localnet",
- "local"
- )
+ .description("deploy the program")
.action(async (opts) => {
+ // show current network
+ showNetwork()
+
if (fs.existsSync(deployedPath)) {
- const programId = fs.readFileSync(deployedPath).toString()
- console.log("already deployed, program id: ", color(programId, "blue"))
- console.log(color("if you want to deployed agian, try `npm run clean`", "red", true))
- return
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+ log(`already deployed, program id: ${color(deployed.programId, "blue")}`)
+ error("if you want to deployed again, try `npm run clean:deployed`")
}
- const { network } = opts
const res = checkRole("payer")
if (!res || !res.exist) {
- return console.error(`role ${color("payer", "blue")} not created`)
+ error(`role [payer] not created`)
}
const fileData = fs.readFileSync(res.walletPath)
const payer = JSON.parse(fileData.toString())
if (!fs.existsSync(sofilePath)) {
- return console.error(`${color("program file not exists", "red")}`)
+ error("program file not exists")
}
const programBinary = fs.readFileSync(sofilePath)
@@ -196,39 +213,355 @@ cli
const conn = await connectTo(network)
const fees = await calculatePayfees(programBinary.length, conn)
- let balance = await conn.getBalance(new PublicKey(payer.publicKey))
+ let balance = await conn.getBalance(new PublicKey(payer.pubkey))
- console.log(`payer wallet: ${color(payer.publicKey, "blue")}, balance: ${color(balance, "blue")}`)
- console.log(`deploy payfees: ${color(fees, "blue")}`)
+ log(`payer wallet: ${color(payer.pubkey, "blue")}, balance: ${color(balance, "blue")}`)
+ log(`deploy payfees: ${color(fees, "blue")}`)
if (balance < fees) {
- return console.log(color("insufficient balance to pay fees", "red"))
+ error("insufficient balance to pay fees")
}
- console.log("deploying...")
+ log("deploying...")
const wallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
const bpfLoader = new BPFLoader(wallet)
const programAccount = await bpfLoader.load(programBinary)
- console.log(`deploy success, program id: ${color(programAccount.publicKey.toBase58(), "blue")}`)
- fs.writeFileSync(deployedPath, programAccount.publicKey.toBase58())
+ log(`deploy success, program id: ${color(programAccount.publicKey.toBase58(), "blue")}`)
+ fs.writeFileSync(deployedPath, JSON.stringify({
+ network,
+ programId: programAccount.publicKey.toBase58()
+ }))
})
cli
- .command("init-aggregator")
- .description("initialize aggregator to the program")
- .option(
- "-n,--network ",
- "deploy on which network (local|devnet|mainnet), default is localnet",
- "local"
- )
- .action(async (opts) => {
- const { network } = opts
- const res = checkRole("aggregator")
- if (!res || !res.exist) {
- return console.error(`role ${color("aggregator", "blue")} not created`)
+ .command("add-aggregator")
+ .description("add an aggregator")
+ .action(async () => {
+ // show current network
+ showNetwork()
+
+ if (!fs.existsSync(deployedPath)) {
+ error("program haven't deployed yet")
}
+
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+
+ if (deployed.network != network) {
+ error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
+ }
+
+ if (!deployed.programId) {
+ error("program haven't deployed yet")
+ }
+
+ 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")}`)
+ fs.writeFileSync(deployedPath, JSON.stringify({
+ ...deployed,
+ pairs: (deployed.pairs || []).concat([{
+ pairName: description.trim(),
+ aggregator: aggregator.toBase58()
+ }])
+ }))
+
+ })
+
+cli
+ .command("aggregators")
+ .description("show all aggregators")
+ .action(() => {
+ // show current network
+ showNetwork()
+
+ if (!fs.existsSync(deployedPath)) {
+ error("program haven't deployed yet")
+ }
+
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+
+ if (deployed.network != network) {
+ error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
+ }
+
+ if (!deployed.programId) {
+ error("program haven't deployed yet")
+ }
+
+ log(deployed.pairs)
+ })
+
+cli
+ .command("add-oracle")
+ .description("add an oracle to aggregator")
+ .action(async () => {
+ // show current network
+ showNetwork()
+
+ if (!fs.existsSync(deployedPath)) {
+ error("program haven't deployed yet")
+ }
+
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+
+ if (deployed.network != network) {
+ error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
+ }
+
+ if (!deployed.programId) {
+ error("program haven't deployed yet")
+ }
+
+ if (!deployed.pairs) {
+ error("no aggregators")
+ }
+
+ let res = checkRole("payer")
+ if (!res || !res.exist) {
+ error(`role ${color("payer", "blue")} not created`)
+ }
+ const payer = JSON.parse(fs.readFileSync(res.walletPath).toString())
+
+ res = checkRole("aggregatorOwner")
+ if (!res || !res.exist) {
+ error(`role ${color("aggregatorOwner", "blue")} not created, please create the role first`)
+ }
+ const aggregatorOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
+
+ res = checkRole("oracleOwner")
+ if (!res || !res.exist) {
+ error(`role ${color("oracleOwner", "blue")} not created, please create the role first`)
+ }
+ const oracleOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
+
+ const inputs = await inquirer
+ .prompt([
+ { message: "Choose an aggregator", type: "list", name: "aggregator", choices: () => {
+ return deployed.pairs.map(p => ({ name: p.pairName.trim() + ` [${p.aggregator}]`, value: p.aggregator }))
+ }},
+ { message: "Oracle name (eg. Solink)", type: "input", name: "oracleName", validate: (input) => {
+ if (!input) {
+ return "oracle name cannot be empty"
+ }
+ return true
+ } }
+ ])
+
+ const { oracleName, aggregator } = inputs
+
+ const conn = await connectTo(network)
+
+ const payerWallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
+ const aggregatorOwnerWallet = await Wallet.fromMnemonic(aggregatorOwner.mnemonic, conn)
+
+ const payerWalletBalance = await conn.getBalance(payerWallet.pubkey)
+ const fees = await calculatePayfees(OracleLayout.span, conn)
+
+ if (payerWalletBalance < fees) {
+ error("insufficient balance to pay fees")
+ }
+
+ const program = new FluxAggregator(payerWallet, new PublicKey(deployed.programId))
+
+ log("add oracle...")
+ const oracle = await program.addOracle({
+ owner: new PublicKey(oracleOwner.pubkey),
+ description: oracleName.substr(0,32).padEnd(32),
+ aggregator: new PublicKey(aggregator),
+ aggregatorOwner: aggregatorOwnerWallet.account,
+ })
+
+ log(`add oracle success, pubkey: ${color(oracle.toBase58(), "blue")}, owner: ${color(oracleOwner.pubkey, "blue")}`)
+ fs.writeFileSync(deployedPath, JSON.stringify({
+ ...deployed,
+ oracles: (deployed.oracles || []).concat([{
+ name: oracleName,
+ aggregator,
+ pubkey: oracle.toBase58()
+ }])
+ }))
+ })
+
+cli
+ .command("oracles")
+ .description("show all oracles")
+ .action(() => {
+ // show current network
+ showNetwork()
+
+ if (!fs.existsSync(deployedPath)) {
+ error("program haven't deployed yet")
+ }
+
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+
+ if (deployed.network != network) {
+ error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
+ }
+
+ if (!deployed.programId) {
+ error("program haven't deployed yet")
+ }
+
+ log(deployed.oracles)
+ })
+
+cli
+ .command("aggregatorInfo")
+ .description("show aggregatorInfo")
+ .action(async () => {
+ // show current network
+ showNetwork()
+
+ if (!fs.existsSync(deployedPath)) {
+ error("program haven't deployed yet")
+ }
+
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+
+ if (deployed.network != network) {
+ error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
+ }
+
+ if (!deployed.programId) {
+ error("program haven't deployed yet")
+ }
+
+ const inputs = await inquirer
+ .prompt([
+ { message: "Choose an aggregator", type: "list", name: "aggregator", choices: () => {
+ return deployed.pairs.map(p => ({ name: p.pairName.trim() + ` [${p.aggregator}]`, value: p.aggregator }))
+ }},
+ ])
+
+ const { aggregator } = inputs
+ const conn = await connectTo(network)
+
+ const accountInfo = await conn.getAccountInfo(new PublicKey(aggregator))
+
+ log(decodeAggregatorInfo(accountInfo))
+ })
+
+cli
+ .command("feed")
+ .description("oracle feeds to aggregator")
+ .action(async () => {
+ // show current network
+ showNetwork()
+
+ if (!fs.existsSync(deployedPath)) {
+ error("program haven't deployed yet")
+ }
+
+ const deployed = JSON.parse(fs.readFileSync(deployedPath).toString())
+
+ if (deployed.network != network) {
+ error("deployed network not match, please try `npm run clean:deployed`, and deploy again")
+ }
+
+ if (!deployed.programId) {
+ error("program haven't deployed yet")
+ }
+
+ const inputs = await inquirer
+ .prompt([
+ { message: "Choose an oracle", type: "list", name: "oracle", choices: () => {
+ return deployed.oracles.map(p => ({ name: p.name+ ` [${p.pubkey}]`, value: `${p.pubkey}|${p.aggregator}` }))
+ }},
+ ])
+
+ const tmpArr = inputs.oracle.split("|")
+
+ let res = checkRole("payer")
+ if (!res || !res.exist) {
+ error(`role ${color("payer", "blue")} not created`)
+ }
+ const payer = JSON.parse(fs.readFileSync(res.walletPath).toString())
+
+ res = checkRole("oracleOwner")
+ if (!res || !res.exist) {
+ error(`role ${color("oracleOwner", "blue")} not created, please create the role first`)
+ }
+ const oracleOwner = JSON.parse(fs.readFileSync(res.walletPath).toString())
+
+ let oracle = tmpArr[0], aggregator = tmpArr[1]
+
+ let pair = ""
+ deployed.pairs.map((p) => {
+ if (p.aggregator == aggregator) {
+ pair = p.pairName
+ }
+ })
+
+ const conn = await connectTo(network)
+
+ const payerWallet = await Wallet.fromMnemonic(payer.mnemonic, conn)
+ const oracleOwnerWallet = await Wallet.fromMnemonic(oracleOwner.mnemonic, conn)
+
+ feed.start({
+ oracle: new PublicKey(oracle),
+ oracleOwner: oracleOwnerWallet.account,
+ aggregator: new PublicKey(aggregator),
+ pair,
+ payerWallet,
+ programId: new PublicKey(deployed.programId)
+ })
})
cli.parse(process.argv)
\ No newline at end of file
diff --git a/src/deployed.json b/src/deployed.json
new file mode 100644
index 0000000..9adaa90
--- /dev/null
+++ b/src/deployed.json
@@ -0,0 +1 @@
+{"network":"local","programId":"95mRqquh7vGi31i87g2tnozV9ngap9GtnkpsHp31kFt1","pairs":[{"pairName":"ETH/USD","aggregator":"GNUKT4Ug3574s6tuWXtPhC9u1VPWgx4XXVCcD8usXHGW"}],"oracles":[{"name":"Solink","aggregator":"GNUKT4Ug3574s6tuWXtPhC9u1VPWgx4XXVCcD8usXHGW","pubkey":"5fs1WPxopQfgRWujfShfh6qLuPGkHjyMYbgxtzZyNtrp"}]}
\ No newline at end of file
diff --git a/src/feed.ts b/src/feed.ts
new file mode 100644
index 0000000..df3071a
--- /dev/null
+++ b/src/feed.ts
@@ -0,0 +1,90 @@
+import { PublicKey, Account, Wallet } from "solray"
+import WebSocket from "ws"
+
+import { decodeOracleInfo } from "./utils"
+
+import FluxAggregator from "./FluxAggregator"
+
+let nextSubmitTime = new Date().getTime()
+let submiting = false
+const submitInterval = 10 * 1000
+
+interface StartParams {
+ oracle: PublicKey;
+ oracleOwner: Account;
+ aggregator: PublicKey;
+ pair: string;
+ payerWallet: Wallet;
+ programId: PublicKey;
+}
+
+export async function start(params: StartParams) {
+ const {
+ oracle,
+ oracleOwner,
+ aggregator,
+ pair,
+ payerWallet,
+ programId,
+ } = params
+
+ console.log("ready to feeds...")
+ const ws = new WebSocket("wss://ws-feed.pro.coinbase.com")
+
+ const program = new FluxAggregator(payerWallet, programId)
+
+ ws.on("open", () => {
+ console.log(`${pair} price feed connected`)
+ ws.send(JSON.stringify({
+ "type": "subscribe",
+ "product_ids": [
+ pair.replace("/", "-").toUpperCase(),
+ ],
+ "channels": [
+ "ticker"
+ ]
+ }))
+ })
+
+ ws.on("message", (data) => {
+ const json = JSON.parse(data)
+ if (!json || !json.price) {
+ return console.log(data)
+ }
+
+ if (submiting) return false
+
+ console.log("new price:", json.price)
+ let now = new Date().getTime()
+ if (now < nextSubmitTime) {
+ console.log("submit cooling...")
+ return false
+ }
+
+ submiting = true
+
+ program.submit({
+ aggregator,
+ oracle,
+ submission: BigInt(parseInt((json.price * 100) as any)),
+ owner: oracleOwner,
+ }).then(() => {
+ console.log("submit success!")
+ nextSubmitTime = now + submitInterval
+ payerWallet.conn.getAccountInfo(oracle).then((accountInfo) => {
+ console.log("oracle info:", decodeOracleInfo(accountInfo))
+ })
+
+ }).catch((err) => {
+ console.log(err)
+ }).finally(() => {
+ submiting = false
+ })
+
+
+ })
+
+ ws.on("close", (error) => {
+ console.error(error)
+ })
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index ef730d8..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import dotenv from "dotenv"
-
-import {
- Wallet, solana, NetworkName, BPFLoader,
- PublicKey, Deployer, Account,
-} from "solray"
-
-import { promises as fs } from "fs"
-import { calculatePayfees, decodeAggregatorInfo, sleep } from "./utils"
-
-import FluxAggregator, { AggregatorLayout } from "./FluxAggregator"
-
-dotenv.config()
-
-const { NETWORK, SOLANA_PAYER_MNEMONIC } = process.env
-
-const args = process.argv.splice(2)
-const cmd = args[0]
-
-const params = args[1]
-
-// so file path
-const soPath = "../build/flux_aggregator.so"
-
-async function main() {
-
- if (!SOLANA_PAYER_MNEMONIC || !NETWORK) {
- throw new Error("Config error.")
- }
-
- const conn = solana.connect(NETWORK as NetworkName)
- const wallet = await Wallet.fromMnemonic(SOLANA_PAYER_MNEMONIC, conn)
- console.log("using wallet", wallet.address)
-
- let walletBalance = await conn.getBalance(wallet.pubkey)
- console.log("wallet banalce:", walletBalance)
-
- const deployer = await Deployer.open("deploy.json")
-
- const programBinary = await fs.readFile(soPath)
-
- let fees = await calculatePayfees(programBinary.length, conn)
-
- const programAccount = await deployer.ensure("programAccount", async () => {
- console.log("loading program... Network:", NETWORK)
-
- if (walletBalance < fees) {
- // throw new Error("Insufficient balance to pay fees");
- // get airdrop
- console.log("insufficient balance to pay fees, request airdrop...")
- await conn.requestAirdrop(wallet.pubkey, fees)
- await sleep(1000)
- }
-
- const bpfLoader = new BPFLoader(wallet)
-
- const account = await bpfLoader.load(programBinary)
-
- return account
- })
-
- console.log("program loaded:", programAccount.publicKey.toBase58())
-
- const program = new FluxAggregator(wallet, programAccount.publicKey)
-
- const aggregatorOwner = new Account()
- console.log("initialize aggregator to owner:", aggregatorOwner.publicKey.toBase58())
-
- walletBalance = await conn.getBalance(wallet.pubkey)
- fees = await calculatePayfees(AggregatorLayout.span, conn)
-
- if (walletBalance < fees) {
- console.log("insufficient balance to pay fees, request airdrop...")
- await conn.requestAirdrop(wallet.pubkey, fees)
- await sleep(1000)
- }
-
- const aggregator = await program.initialize({
- submitInterval: 6,
- minSubmissionValue: BigInt(1),
- maxSubmissionValue: BigInt(99999),
- description: "ETH/USDT".padEnd(32),
- owner: aggregatorOwner
- })
- console.log("aggregator initialized, pubkey:", aggregator.toBase58())
-
- const oracleOwner = new Account()
- console.log("add an oracle...")
- const oracle = await program.addOracle({
- owner: oracleOwner.publicKey,
- description: "Solink".padEnd(32),
- aggregator,
- aggregatorOwner,
- })
-
- console.log("oracle added, pubkey:", oracle.toBase58(), ", owner: ", oracleOwner.publicKey.toBase58())
-
- console.log("oracle submiting...")
- await program.submit({
- aggregator,
- oracle,
- submission: BigInt(123),
- owner: oracleOwner,
- })
-
- console.log("submit success! get aggregator info...")
-
- const accountInfo = await conn.getAccountInfo(aggregator)
-
- console.log("aggregator info:", decodeAggregatorInfo(accountInfo))
-}
-
-
-main().catch(err => console.log({ err }))
\ No newline at end of file
diff --git a/src/oracle.ts b/src/oracle.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/utils.ts b/src/utils.ts
index 0b8d38e..2f8623c 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,6 +1,6 @@
import { Connection, BpfLoader, PublicKey } from "@solana/web3.js"
-import { AggregatorLayout, SubmissionLayout } from "./FluxAggregator"
+import { AggregatorLayout, SubmissionLayout, OracleLayout } from "./FluxAggregator"
import { solana, Wallet, NetworkName } from "solray"
@@ -92,6 +92,22 @@ export function decodeAggregatorInfo(accountInfo) {
}
}
+export function decodeOracleInfo(accountInfo) {
+ const data = Buffer.from(accountInfo.data)
+
+ const oracle = OracleLayout.decode(data)
+
+ oracle.submission = oracle.submission.readBigUInt64LE().toString()
+ oracle.nextSubmitTime = oracle.nextSubmitTime.readBigUInt64LE().toString()
+ oracle.description = oracle.description.toString()
+ oracle.isInitialized = oracle.isInitialized != 0
+ oracle.withdrawable = oracle.withdrawable.readBigUInt64LE().toString()
+ oracle.aggregator = new PublicKey(oracle.aggregator).toBase58()
+ oracle.owner = new PublicKey(oracle.owner).toBase58()
+
+ return oracle
+}
+
export async function connectTo(network: NetworkName): Promise {
const conn = solana.connect(network as NetworkName)
return conn
diff --git a/src/wallets/aggregatorOwner.json b/src/wallets/aggregatorOwner.json
new file mode 100644
index 0000000..383860e
--- /dev/null
+++ b/src/wallets/aggregatorOwner.json
@@ -0,0 +1 @@
+{"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"}
\ No newline at end of file
diff --git a/src/wallets/oracleOwner.json b/src/wallets/oracleOwner.json
new file mode 100644
index 0000000..0c32054
--- /dev/null
+++ b/src/wallets/oracleOwner.json
@@ -0,0 +1 @@
+{"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"}
\ No newline at end of file
diff --git a/src/wallets/payer.json b/src/wallets/payer.json
new file mode 100644
index 0000000..c6208c1
--- /dev/null
+++ b/src/wallets/payer.json
@@ -0,0 +1 @@
+{"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"}
\ No newline at end of file