feed polling and oracle removal

This commit is contained in:
De Facto 2020-12-10 11:39:52 +00:00
parent 26f529d304
commit 3e097081c4
10 changed files with 126 additions and 102 deletions

2
.env
View File

@ -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"
ORACLE_MNEMONIC="amount smoke bar coil current trial toward minimum model pass moral liberty"

11
.gitignore vendored
View File

@ -1,4 +1,7 @@
/node_modules
/program/target
/integration-example/target
yarn.lock
node_modules
target
target
yarn.lock
build
.yarn
dist

View File

@ -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

Binary file not shown.

View File

@ -14,5 +14,9 @@
"tokenAccount": {
"pubkey": "A4xJLiJrtVigYjJ2NRUrcFza7eB7Y8XeGZh5KmkoxDs1",
"secret": "19c19ac59d379f3d669b6b5957bbf99060b2b15dd4d266b3f76e164f665c512986bc739598f0e5f0252b3e2fbd0c54a9a08b295a4c5c2901b91c6e3d1e8ace5c"
},
"oracle[0]": {
"pubkey": "4jWLbd2Vm98RrqunVvaSXZuP1AFbgQSM2hAHMvZSdNCu",
"secret": "83369edeffc2f5b870da6595cd7a916723566a1307e722f29e7db6bc8fd01b643776a09078c98230dd23a3aeab3e870e7fb6651ddae3cbf772afa4eba4735e16"
}
}

View File

@ -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"
}

View File

@ -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<PublicKey> {
public async addOracle(params: AddOracleParams): Promise<Account> {
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<void> {
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,
]);
}

View File

@ -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 <number>", "minSubmissionValue", "0")
.option("--maxSubmissionValue <number>", "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 <string>", "oracle name")
.option("--oracleOwner <string>", "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 <number>", "remove oracle from index (0-20)")
.option("--feedAddress <string>", "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 <string>", "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 <string>", "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 <number>", "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({

View File

@ -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<void> {
}
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<Wall
export async function openDeployer(): Promise<Deployer> {
const deployFile = process.env.DEPLOY_FILE
if (!deployFile) {
throw new Error(`Set DEPLOY_FILE in .env`)
}

View File

@ -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/**/*"
]
}
}