diff --git a/.env b/.env index 7e6cb3a..08ebdf7 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ NETWORK=dev DEPLOY_FILE=deploy.json ADMIN_MNEMONIC="summer fuel twin history item learn flip marble ginger knee mix ten" -ORACLE_MNEMONIC="pet retreat peasant wing search rug dwarf high city pill giggle dinner" \ No newline at end of file +ORACLE_MNEMONIC="amount smoke bar coil current trial toward minimum model pass moral liberty" \ No newline at end of file diff --git a/.gitignore b/.gitignore index b808e10..e293b16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -/node_modules -/program/target -/integration-example/target -yarn.lock \ No newline at end of file +node_modules +target +target +yarn.lock +build +.yarn +dist \ No newline at end of file diff --git a/README.md b/README.md index 57ef4d2..de0b57f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ yarn build:program ``` yarn solink deploy-program -deployed aggregator program. program id: 9KXbVqUrMgtti7Jx4rrV1NqXjQNxWaKgtYCEwJ8AESS5 +deployed aggregator program. program id: HFHbe2uckzz9Xh633mbJPYcukzpyJRVcwL87fUrVddiq ``` Create the `btc:usd` feed (that accepts max and min u64 as valid submission values): @@ -54,7 +54,7 @@ yarn solink add-aggregator \ --minSubmissionValue 0 \ --maxSubmissionValue 18446744073709551615 -feed initialized, pubkey: AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2 +feed initialized, pubkey: 2jReuMRoYi3pKTF8YLnZEvT2bXcw56SdBxvssrVzu41v ``` ## Adding an oracle @@ -83,17 +83,36 @@ Next we create a new oracle to the feed we've created previously, and set its ow ``` yarn solink add-oracle \ --index 0 \ - --feedAddress AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2 \ + --feedAddress 2jReuMRoYi3pKTF8YLnZEvT2bXcw56SdBxvssrVzu41v \ --oracleName solink-test \ --oracleOwner FosLwbttPgkEDv36VJLU3wwXcBSSoUGkh7dyZPsXNtT4 -added oracle. pubkey: 4vH5L2jSNXGfcCx42N4sqPiMzEbp1PaQjQ6XngDBu8zR +added oracle. pubkey: 4jWLbd2Vm98RrqunVvaSXZuP1AFbgQSM2hAHMvZSdNCu ``` +Start submitting data from a price feed (e.g. coinbase BTC-USDT): + ``` yarn solink feed \ - --feedAddress AUK9X6QLgauAUvEA3Ajc91fZytb9ccA7qVR72ErDFNg2 \ - --oracleAddress 4vH5L2jSNXGfcCx42N4sqPiMzEbp1PaQjQ6XngDBu8zR + --feedAddress 2jReuMRoYi3pKTF8YLnZEvT2bXcw56SdBxvssrVzu41v \ + --oracleAddress 4jWLbd2Vm98RrqunVvaSXZuP1AFbgQSM2hAHMvZSdNCu +``` + +## Read price + +Poll the latest aggregated (median) value from a feed: + +``` +yarn solink feed-poll \ + --feedAddress 2jReuMRoYi3pKTF8YLnZEvT2bXcw56SdBxvssrVzu41v +``` + +## Remove oracle + +``` +yarn solink remove-oracle \ + --index 0 \ + --feedAddress 2jReuMRoYi3pKTF8YLnZEvT2bXcw56SdBxvssrVzu41v ``` ## Test Token diff --git a/build/flux_aggregator.so b/build/flux_aggregator.so index 5ab1078..6e7d895 100755 Binary files a/build/flux_aggregator.so and b/build/flux_aggregator.so differ diff --git a/deploy.json b/deploy.json index 68f10f3..c4d2cd5 100644 --- a/deploy.json +++ b/deploy.json @@ -14,5 +14,9 @@ "tokenAccount": { "pubkey": "A4xJLiJrtVigYjJ2NRUrcFza7eB7Y8XeGZh5KmkoxDs1", "secret": "19c19ac59d379f3d669b6b5957bbf99060b2b15dd4d266b3f76e164f665c512986bc739598f0e5f0252b3e2fbd0c54a9a08b295a4c5c2901b91c6e3d1e8ace5c" + }, + "oracle[0]": { + "pubkey": "4jWLbd2Vm98RrqunVvaSXZuP1AFbgQSM2hAHMvZSdNCu", + "secret": "83369edeffc2f5b870da6595cd7a916723566a1307e722f29e7db6bc8fd01b643776a09078c98230dd23a3aeab3e870e7fb6651ddae3cbf772afa4eba4735e16" } } \ No newline at end of file diff --git a/package.json b/package.json index 6d77b3d..10aebe0 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,12 @@ "buffer-layout": "^1.2.0", "commander": "^6.2.0", "dotenv": "^8.2.0", - "solray": "git+https://github.com/defactojob/solray.git" + "solray": "git+https://github.com/defactojob/solray.git", + "ws": "^7.4.1" }, "devDependencies": { "@tsconfig/recommended": "^1.0.1", - "@types/node": "^14.14.10", + "@types/node": "^14.14.12", "ts-node": "^9.1.1", "typescript": "^4.1.2" } diff --git a/src/FluxAggregator.ts b/src/FluxAggregator.ts index 3c94fb9..5583098 100644 --- a/src/FluxAggregator.ts +++ b/src/FluxAggregator.ts @@ -73,10 +73,8 @@ interface RemoveOracleParams { // oracle index index: number; aggregator: PublicKey; - // The oracle key - oracle: PublicKey; // To prove you are the aggregator owner - authority: Account; + authority?: Account; } interface RemoveOracleInstructionParams extends RemoveOracleParams { @@ -172,7 +170,7 @@ export default class FluxAggregator extends BaseProgram { ]); } - public async addOracle(params: AddOracleParams): Promise { + public async addOracle(params: AddOracleParams): Promise { const account = new Account(); await this.sendTx([ @@ -187,7 +185,7 @@ export default class FluxAggregator extends BaseProgram { }) ], [this.account, account, params.aggregatorOwner]); - return account.publicKey; + return account; } public async oracleInfo(pubkey: PublicKey) { @@ -227,14 +225,14 @@ export default class FluxAggregator extends BaseProgram { public async removeOracle(params: RemoveOracleParams): Promise { await this.sendTx([ this.removeOracleInstruction(params) - ], [this.account, params.authority]); - + ], [this.account, params.authority || this.wallet.account]); } private removeOracleInstruction(params: RemoveOracleInstructionParams): TransactionInstruction { const { index, authority, + aggregator, } = params; const layout = BufferLayout.struct([ @@ -246,7 +244,9 @@ export default class FluxAggregator extends BaseProgram { instruction: 2, // remove oracle instruction index, }, [ - { write: authority }, + // + { write: aggregator }, + authority || this.wallet.account, ]); } diff --git a/src/cli.ts b/src/cli.ts index bbb8505..c077d86 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,9 +3,9 @@ import { Command, option } from "commander" import fs from "fs" import path from "path" -import { - BPFLoader, PublicKey, Wallet, NetworkName, - solana, Deployer, SPLToken, ProgramAccount +import { + BPFLoader, PublicKey, Wallet, NetworkName, + solana, Deployer, SPLToken, ProgramAccount } from "solray" import dotenv from "dotenv" @@ -16,6 +16,7 @@ import { decodeAggregatorInfo, walletFromEnv, openDeployer, + sleep, } from "./utils" import * as feed from "./feed" @@ -48,7 +49,15 @@ class AppContext { constructor(public deployer: Deployer, public wallet: Wallet) { } - get aggregatorProgram() { + get aggregatorProgramID() { + return this.aggregatorProgramAccount.publicKey + } + + get aggregator() { + return new FluxAggregator(this.wallet, this.aggregatorProgramID) + } + + get aggregatorProgramAccount() { const program = this.deployer.account(AppContext.AGGREGATOR_PROGRAM) if (program == null) { @@ -129,7 +138,7 @@ cli .option("--minSubmissionValue ", "minSubmissionValue", "0") .option("--maxSubmissionValue ", "maxSubmissionValue", "18446744073709551615") .action(async (opts) => { - const { deployer, wallet, aggregatorProgram } = await AppContext.forAdmin() + const { deployer, wallet, aggregatorProgramAccount: aggregatorProgram } = await AppContext.forAdmin() const { feedName, submitInterval, minSubmissionValue, maxSubmissionValue } = opts @@ -180,25 +189,41 @@ cli .option("--oracleName ", "oracle name") .option("--oracleOwner ", "oracle owner address") .action(async (opts) => { - const { wallet, aggregatorProgram } = await AppContext.forAdmin() + const { wallet, aggregator, deployer } = await AppContext.forAdmin() const { index, oracleName, oracleOwner, feedAddress } = opts if (!index || index < 0 || index > 21) { - error("invalid index (0-20)") + error("invalid index. requires (0-20)") } - const program = new FluxAggregator(wallet, aggregatorProgram.publicKey) log("add oracle...") - const oracle = await program.addOracle({ - index, - owner: new PublicKey(oracleOwner), - description: oracleName.substr(0, 32).padEnd(32), - aggregator: new PublicKey(feedAddress), - aggregatorOwner: wallet.account, + const oracle = await deployer.ensure(`oracle[${index}]`, async () => { + return aggregator.addOracle({ + index, + owner: new PublicKey(oracleOwner), + description: oracleName.substr(0, 32).padEnd(32), + aggregator: new PublicKey(feedAddress), + aggregatorOwner: wallet.account, + }) }) - log(`added oracle. pubkey: ${color(oracle.toBase58(), "blue")}`) + log(`added oracle. pubkey: ${color(oracle.publicKey.toBase58(), "blue")}`) + }) + +cli + .command("remove-oracle") + .option("--index ", "remove oracle from index (0-20)") + .option("--feedAddress ", "feed to remove oracle from") + .action(async (opts) => { + const { index, feedAddress } = opts + + const { aggregator } = await AppContext.forAdmin() + + await aggregator.removeOracle({ + aggregator: new PublicKey(feedAddress), + index, + }) }) // cli @@ -225,43 +250,20 @@ cli // log(deployed.oracles) // }) -// cli -// .command("aggregatorInfo") -// .description("show aggregatorInfo") -// .action(async () => { -// // show current network -// showNetwork() +cli + .command("feed-poll") + .description("poll current feed value") + .option("--feedAddress ", "feed address to submit values to") + .action(async (opts) => { + const { feedAddress } = opts -// if (!fs.existsSync(deployedPath)) { -// error("program haven't deployed yet") -// } + while (true) { + const feedInfo = await conn.getAccountInfo(new PublicKey(feedAddress)) + log(decodeAggregatorInfo(feedInfo)) -// 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)) -// }) + await sleep(1000) + } + }) cli .command("feed") @@ -270,7 +272,7 @@ cli .option("--oracleAddress ", "feed address to submit values to") .action(async (opts) => { - const { wallet, aggregatorProgram } = await AppContext.forOracle() + const { wallet, aggregatorProgramAccount: aggregatorProgram } = await AppContext.forOracle() const { feedAddress, oracleAddress } = opts @@ -289,7 +291,7 @@ cli .description("create test token") .option("--amount ", "amount of the test token") .action(async (opts) => { - const { admin, aggregatorProgram, deployer } = await AdminContext.load() + const { wallet, aggregatorProgramAccount: aggregatorProgram, deployer } = await AppContext.forAdmin() const { amount } = opts @@ -297,18 +299,18 @@ cli error("invalid amount") } - const spltoken = new SPLToken(admin) - + const spltoken = new SPLToken(wallet) + log(`create test token...`) // 1. create token const token = await spltoken.initializeMint({ - mintAuthority: admin.account.publicKey, + mintAuthority: wallet.account.publicKey, decimals: 8, }) // 2. create tokenOwner (program account) const tokenOwner = await ProgramAccount.forSeed( - Buffer.from(token.publicKey.toBuffer()).slice(0, 30), + Buffer.from(token.publicKey.toBuffer()).slice(0, 30), aggregatorProgram.publicKey ) @@ -325,7 +327,7 @@ cli token: token.publicKey, to: tokenAccount.publicKey, amount: BigInt(amount), - authority: admin.account, + authority: wallet.account, }) log({ diff --git a/src/utils.ts b/src/utils.ts index 6b6426f..cdf7206 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,7 @@ import { AggregatorLayout, SubmissionLayout, OracleLayout } from "./FluxAggregat import { solana, Wallet, NetworkName, Deployer } from "solray" -export function getSubmissionValue(submissions: []): number { +export function getMedian(submissions: number[]): number { const values = submissions .filter((s: any) => s.value != 0) .map((s: any) => s.value) @@ -31,47 +31,45 @@ 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().toString() - const maxSubmissionValue = aggregator.maxSubmissionValue.readBigUInt64LE().toString() + const minSubmissionValue = aggregator.minSubmissionValue.readBigUInt64LE() + const maxSubmissionValue = aggregator.maxSubmissionValue.readBigUInt64LE() const submitInterval = aggregator.submitInterval.readInt32LE() - const description = aggregator.description.toString() + const description = (aggregator.description.toString() as String).trim() // decode oracles - let submissions: [] = [] - let oracles: [] = [] + let submissions: any[] = [] let submissionSpace = SubmissionLayout.span - let updateTime = '0' + 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() - submission.time = submission.time.readBigInt64LE().toString() - submission.value = submission.value.readBigInt64LE().toString()*1 if (!submission.oracle.equals(new PublicKey(0))) { - submissions.push(submission as never) - oracles.push(submission.oracle.toBase58() as never) - + submissions.push(submission) } - if (submission.time > updateTime) { - updateTime = submission.time + + if (submission.time > latestUpdateTime) { + latestUpdateTime = submission.time } } return { minSubmissionValue: minSubmissionValue, maxSubmissionValue: maxSubmissionValue, - submissionValue: getSubmissionValue(submissions), + submissionValue: getMedian(submissions), submitInterval, description, - oracles, - updateTime, + oracles: submissions.map(s => s.oracle.toString()), + latestUpdateTime: new Date(Number(latestUpdateTime)*1000), } } @@ -102,7 +100,7 @@ export async function walletFromEnv(key: string, conn: Connection): Promise { const deployFile = process.env.DEPLOY_FILE - + if (!deployFile) { throw new Error(`Set DEPLOY_FILE in .env`) } diff --git a/tsconfig.json b/tsconfig.json index b653409..c1013d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ + "incremental": true, /* Enable incremental compilation */ "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ @@ -67,8 +67,5 @@ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - }, - "include": [ - "./src/**/*" - ] + } }