diff --git a/src/FluxAggregator.ts b/src/FluxAggregator.ts index fcd1638..812b088 100644 --- a/src/FluxAggregator.ts +++ b/src/FluxAggregator.ts @@ -16,40 +16,11 @@ import { SystemProgram, } from "@solana/web3.js" -import { - publicKey, - u64LEBuffer, - uint64, - BufferLayout, -} from "solray/lib/util/encoding" - -import { decodeOracleInfo } from "./utils" - -// @ts-ignore -// import BufferLayout from "buffer-layout"; - import { AggregatorConfig, IAggregatorConfig, schema } from "./schema" import * as encoding from "./schema" import { deserialize, serialize } from "borsh" import { conn } from "./context" -export const AggregatorLayout = BufferLayout.struct([]) - -export const OracleLayout = BufferLayout.struct([ - uint64("nextSubmitTime"), - BufferLayout.blob(32, "description"), - BufferLayout.u8("isInitialized"), - uint64("withdrawable"), - publicKey("aggregator"), - publicKey("owner"), -]) - -export const SubmissionLayout = BufferLayout.struct([ - uint64("time"), - uint64("value"), - publicKey("oracle"), -]) - interface InitializeParams { config: IAggregatorConfig owner: Account diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index b730b1c..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { Command, option } from "commander" - -import fs from "fs" -import path from "path" - -import { - BPFLoader, - PublicKey, - Wallet, - NetworkName, - solana, - Deployer, - SPLToken, - ProgramAccount, -} from "solray" - -import dotenv from "dotenv" - -import FluxAggregator from "./FluxAggregator" - -import { decodeAggregatorInfo, sleep } from "./utils" - -import * as feed from "./feed" -import { AggregatorConfig } from "./schema" - -dotenv.config() - -const cli = new Command() - -const FLUX_AGGREGATOR_SO = path.resolve( - __dirname, - "../build/flux_aggregator.so" -) -const network = (process.env.NETWORK || "local") as NetworkName -const conn = solana.connect(network) - -import { AppContext } from "./context" - -function color(s, c = "black", b = false): string { - // 30m Black, 31m Red, 32m Green, 33m Yellow, 34m Blue, 35m Magenta, 36m Cyanic, 37m White - const cArr = [ - "black", - "red", - "green", - "yellow", - "blue", - "megenta", - "cyanic", - "white", - ] - - let cIdx = cArr.indexOf(c) - let bold = b ? "\x1b[1m" : "" - - return `\x1b[${30 + (cIdx > -1 ? cIdx : 0)}m${bold}${s}\x1b[0m` -} - -function error(message: string) { - console.log("\n") - console.error(color(message, "red")) - console.log("\n") - process.exit() -} - -function log(message: any) { - console.log(message) -} - -cli.command("generate-wallet").action(async () => { - const mnemonic = Wallet.generateMnemonic() - const wallet = await Wallet.fromMnemonic(mnemonic, conn) - - log(`address: ${wallet.address}`) - log(`mnemonic: ${mnemonic}`) -}) - -cli - .command("airdrop
") - .description(`request airdrop to the address`) - .option("-m, --amount ", "request amount in sol (10e9)", "10") - .action(async (address, opts) => { - const dest = new PublicKey(address) - - const { amount } = opts - - log(`requesting 10 sol airdrop to: ${address}`) - await conn.requestAirdrop(dest, amount * 1e9) - log("airdrop success") - }) - -cli - .command("deploy-program") - .description("deploy the aggregator program") - .action(async () => { - const { wallet, deployer } = await AppContext.forAdmin() - - const programAccount = await deployer.ensure( - AppContext.AGGREGATOR_PROGRAM, - async () => { - const programBinary = fs.readFileSync(FLUX_AGGREGATOR_SO) - - log(`deploying ${FLUX_AGGREGATOR_SO}...`) - const bpfLoader = new BPFLoader(wallet) - - return bpfLoader.load(programBinary) - } - ) - - log( - `deployed aggregator program. program id: ${color( - programAccount.publicKey.toBase58(), - "blue" - )}` - ) - }) - -cli - .command("add-aggregator") - .description("create an aggregator") - .option("--feedName ", "feed pair name") - .option("--decimals ", "submission decimals", "2") - .option("--minSubmissions ", "minSubmissions", "1") - .option("--maxSubmissions ", "maxSubmissions", "12") - .option("--rewardAmount ", "rewardAmount", "1") - .option("--restartDelay ", "restartDelay", "1") - .action(async (opts) => { - const { - deployer, - wallet, - aggregatorProgramAccount: aggregatorProgram, - } = await AppContext.forAdmin() - - const { - feedName, - restartDelay, - minSubmissions, - maxSubmissions, - decimals, - rewardAmount, - } = opts - - const aggregator = new FluxAggregator(wallet, aggregatorProgram.publicKey) - - const feed = await deployer.ensure(feedName, async () => { - return aggregator.initialize({ - config: new AggregatorConfig({ - description: feedName, - decimals, - minSubmissions, - maxSubmissions, - restartDelay, - rewardAmount: BigInt(rewardAmount), - }), - owner: wallet.account, - }) - }) - - log(`feed initialized, pubkey: ${color(feed.publicKey.toBase58(), "blue")}`) - }) - -// 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") - .option("--aggregatorAddress ", "aggregator address") - .option("--oracleName ", "oracle name") - .option("--oracleOwner ", "oracle owner address") - .action(async (opts) => { - const { wallet, aggregator, deployer } = await AppContext.forAdmin() - - const { oracleName, oracleOwner, feedAddress } = opts - - log("add oracle...") - const oracle = await aggregator.addOracle({ - oracleOwner: new PublicKey(oracleOwner), - description: oracleName, - aggregator: new PublicKey(feedAddress), - aggregatorOwner: wallet.account, - }) - - log(`added oracle. pubkey: ${color(oracle.publicKey.toBase58(), "blue")}`) - }) - -cli - .command("remove-oracle") - .option("--feedAddress ", "feed to remove oracle from") - .option("--oracleAddress ", "oracle address") - .action(async (opts) => { - const { feedAddress, oracleAddress } = opts - - const { aggregator } = await AppContext.forAdmin() - - await aggregator.removeOracle({ - aggregator: new PublicKey(feedAddress), - oracle: new PublicKey(oracleAddress), - }) - }) - -// 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("feed-poll") - .description("poll current feed value") - .option("--feedAddress ", "feed address to submit values to") - .action(async (opts) => { - const { feedAddress } = opts - - while (true) { - const feedInfo = await conn.getAccountInfo(new PublicKey(feedAddress)) - log(decodeAggregatorInfo(feedInfo)) - - await sleep(1000) - } - }) - -cli - .command("feed") - .description("oracle feeds to aggregator") - .option("--feedAddress ", "feed address to submit values to") - .option("--oracleAddress ", "feed address to submit values to") - .option("--pairSymbol ", "market pair to feed") - .action(async (opts) => { - const { - wallet, - aggregatorProgramAccount: aggregatorProgram, - } = await AppContext.forOracle() - - const { feedAddress, oracleAddress, pairSymbol } = opts - - feed.start({ - oracle: new PublicKey(oracleAddress), - oracleOwner: wallet.account, - feed: new PublicKey(feedAddress), - pairSymbol: pairSymbol, - payerWallet: wallet, - programId: aggregatorProgram.publicKey, - }) - }) - -cli - .command("testToken") - .description("create test token") - .option("--amount ", "amount of the test token") - .action(async (opts) => { - const { - wallet, - aggregatorProgramAccount: aggregatorProgram, - deployer, - } = await AppContext.forAdmin() - - const { amount } = opts - - if (!amount || amount < 0) { - error("invalid amount") - } - - const spltoken = new SPLToken(wallet) - - log(`create test token...`) - // 1. create token - const token = await spltoken.initializeMint({ - mintAuthority: wallet.account.publicKey, - decimals: 8, - }) - - // 2. create tokenOwner (program account) - const tokenOwner = await ProgramAccount.forSeed( - Buffer.from(token.publicKey.toBuffer()).slice(0, 30), - aggregatorProgram.publicKey - ) - - log(`create token acount...`) - // 3. create token account - const tokenAccount = await spltoken.initializeAccount({ - token: token.publicKey, - owner: tokenOwner.pubkey, - }) - - log(`mint ${amount} token to token account...`) - // 4. and then, mint tokens to that account - await spltoken.mintTo({ - token: token.publicKey, - to: tokenAccount.publicKey, - amount: BigInt(amount), - authority: wallet.account, - }) - - log({ - token: token.publicKey.toBase58(), - tokenAccount: tokenAccount.publicKey.toBase58(), - tokenOwner: { - address: tokenOwner.address, - seed: tokenOwner.noncedSeed.toString("hex"), - nonce: tokenOwner.nonce, - }, - }) - }) - -cli.parse(process.argv) diff --git a/src/feed.ts b/src/feed.ts deleted file mode 100644 index 8232188..0000000 --- a/src/feed.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { PublicKey, Account, Wallet } from "solray" -import WebSocket from "ws" - -import { decodeOracleInfo, sleep } from "./utils" - -import FluxAggregator from "./FluxAggregator" - -const submitInterval = 10 * 1000 - -interface StartParams { - oracle: PublicKey; - oracleOwner: Account; - feed: PublicKey; - pairSymbol: string; - payerWallet: Wallet; - programId: PublicKey; -} - -export async function start(params: StartParams) { - const { - oracle, - oracleOwner, - feed, - pairSymbol, - payerWallet, - programId, - } = params - - console.log("connecting to wss://ws-feed.pro.coinbase.com ()") - const ws = new WebSocket("wss://ws-feed.pro.coinbase.com") - - ws.on("open", () => { - console.log(`${pairSymbol} price feed connected`) - ws.send(JSON.stringify({ - "type": "subscribe", - "product_ids": [ - pairSymbol.replace("/", "-").toUpperCase(), - ], - "channels": [ - "ticker" - ] - })) - }) - - // in penny - let curPriceCent = 0 - - ws.on("message", async (data) => { - const json = JSON.parse(data) - if (!json || !json.price) { - return console.log(data) - } - - curPriceCent = Math.floor(json.price * 100) - - console.log("current price:", json.price) - }) - - ws.on("close", (err) => { - console.error(`websocket closed: ${err}`) - process.exit(1) - }) - - const program = new FluxAggregator(payerWallet, programId) - - console.log(await program.oracleInfo(oracle)) - console.log({ owner: oracleOwner.publicKey.toString() }) - - while (true) { - if (curPriceCent == 0) { - await sleep(1000) - } - - try { - await program.submit({ - aggregator: feed, - oracle, - submission: BigInt(curPriceCent), - owner: oracleOwner, - }) - } catch(err) { - console.log(err) - } - - console.log("submit success!") - - payerWallet.conn.getAccountInfo(oracle).then((accountInfo) => { - console.log("oracle info:", decodeOracleInfo(accountInfo)) - }) - - console.log("wait for cooldown success!") - await sleep(submitInterval) - } -} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index afe1a2a..41ae07f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,5 @@ import { Connection, PublicKey } from "@solana/web3.js" -import { - AggregatorLayout, - SubmissionLayout, - OracleLayout, -} from "./FluxAggregator" - import { solana, Wallet, NetworkName, Deployer } from "solray" export function getMedian(submissions: number[]): number { @@ -33,67 +27,6 @@ export function sleep(ms: number): Promise { }) } -export function decodeAggregatorInfo(accountInfo) { - const data = Buffer.from(accountInfo.data) - const aggregator = AggregatorLayout.decode(data) - - const minSubmissionValue = aggregator.minSubmissionValue.readBigUInt64LE() - const maxSubmissionValue = aggregator.maxSubmissionValue.readBigUInt64LE() - const submitInterval = aggregator.submitInterval.readInt32LE() - const description = (aggregator.description.toString() as String).trim() - - // decode oracles - let submissions: any[] = [] - let submissionSpace = SubmissionLayout.span - let latestUpdateTime = BigInt(0) - - for (let i = 0; i < aggregator.submissions.length / submissionSpace; i++) { - let submission = SubmissionLayout.decode( - aggregator.submissions.slice( - i * submissionSpace, - (i + 1) * submissionSpace - ) - ) - - submission.oracle = new PublicKey(submission.oracle) - submission.time = submission.time.readBigInt64LE() - submission.value = submission.value.readBigInt64LE() - - if (!submission.oracle.equals(new PublicKey(0))) { - submissions.push(submission) - } - - if (submission.time > latestUpdateTime) { - latestUpdateTime = submission.time - } - } - - return { - minSubmissionValue: minSubmissionValue, - maxSubmissionValue: maxSubmissionValue, - submissionValue: getMedian(submissions), - submitInterval, - description, - oracles: submissions.map((s) => s.oracle.toString()), - latestUpdateTime: new Date(Number(latestUpdateTime) * 1000), - } -} - -export function decodeOracleInfo(accountInfo) { - const data = Buffer.from(accountInfo.data) - - const oracle = OracleLayout.decode(data) - - 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 walletFromEnv( key: string, conn: Connection