updated feed-walkthrough example

This commit is contained in:
Conner Gallagher 2023-03-01 13:53:07 -07:00
parent f84eea9549
commit 9fdfbbe0db
9 changed files with 344 additions and 91 deletions

View File

@ -1,59 +1,130 @@
# Switchboard-V2 Feed Walkthrough
<div align="center">
<a href="#">
<img height="170" src="https://github.com/switchboard-xyz/sbv2-core/raw/main/website/static/img/icons/switchboard/avatar.svg" />
</a>
This example will walk you through
<h1>Sbv2 Feed Walkthrough</h1>
- creating a personal oracle queue with a crank
- add a SOL/USD data feed onto the crank
- spin up a docker environment to run your own oracle
- fulfill your update request on-chain
<p>An example showing how to create your own feed using Switchboard.</p>
<p>
<a href="https://discord.gg/switchboardxyz">
<img alt="Discord" src="https://img.shields.io/discord/841525135311634443?color=blueviolet&logo=discord&logoColor=white">
</a>
<a href="https://twitter.com/switchboardxyz">
<img alt="Twitter" src="https://img.shields.io/twitter/follow/switchboardxyz?label=Follow+Switchboard" />
</a>
</p>
<h4>
<strong>Npm: </strong><a href="https://www.npmjs.com/package/@switchboard-xyz/solana.js">npmjs.com/package/@switchboard-xyz/solana.js</a>
</h4>
<h4>
<strong>Typedocs: </strong><a href="https://docs.switchboard.xyz/api/@switchboard-xyz/solana.js">docs.switchboard.xyz/api/@switchboard-xyz/solana.js</a>
</h4>
<h4>
<strong>Sbv2 Solana SDK: </strong><a href="https://github.com/switchboard-xyz/sbv2-solana">github.com/switchboard-xyz/sbv2-solana</a>
</h4>
</div>
## Install
```bash
npm i
```
## Usage
```bash
ts-node src/main [PAYER_KEYPAIR_PATH]
```
### Simulate an OracleJob
where **PAYER_KEYPAIR_PATH** is the location of your Solana keypair, defaulting
to `~/.config/solana/id.json` if not provided
When prompted, run the docker compose script in a new shell to start your local
oracle then confirm the prompt to turn the crank and request an update on-chain.
The oracle is ready to fulfill updates when it sees the following logs:
Edit the OracleJob file `src/oracle-job.json`, then run
```bash
{"timestamp":"2022-09-23T19:24:11.874Z","level":"info","message":"Loaded 1000 nonce accounts"}
{"timestamp":"2022-09-23T19:24:11.885Z","level":"info","message":"started health check handler"}
{"timestamp":"2022-09-23T19:24:11.886Z","level":"info","message":"Heartbeat routine started with an interval of 15 seconds."}
{"timestamp":"2022-09-23T19:24:11.887Z","level":"info","message":"Watching event: AggregatorOpenRoundEvent ..."}
{"timestamp":"2022-09-23T19:24:11.893Z","level":"info","message":"Watching event: VrfRequestRandomnessEvent ..."}
{"timestamp":"2022-09-23T19:24:11.894Z","level":"info","message":"Using default performance monitoring"}
ts-node src/simulate
```
Example Output:
### Create a Feed on Devnet
You can create your own feeds using the devnet permissionless network. This
network does _NOT_ require the queue authority to grant you permissions so you
are free to use it as a testing environment.
You do **_NOT_** need to run your own oracles for this network.
Edit the OracleJob file `src/oracle-job.json`, then run
```bash
$ ts-node src/main
######## Switchboard Setup ########
Program State BYM81n8HvTJuqZU1PmTVcwZ9G8uoji7FKM6EaPkwphPt
Oracle Queue AVbBmSeKJppRcphaPY1fPbFQW48Eg851G4XfqyTPMZNF
Crank 6fNsrJhaB2MPpwpcxW7AL5zyoiq7Gyz2mM6q3aVz7xxh
Oracle CmTr9FSeuhMPBLEPa3o2M71RwRnBz6LMcsfzHaW721Ak
Permission 2pC5ESkVKGx4yowGrVB21f6eXaaMRQY5cBazfqn1bAQs
Aggregator (SOL/USD) FLixyyJVzfCF4PmDG2VcFm1LUBu1aBTXox3oCWNVU88m
Permission EVerqanwRrHRvtPXDRdFHPc7VnXuyEPRr9XA5udpFA4E
Lease FC6SfAEuoB1SoZAnCqkMyyYnSfLSy8KfPUFH9SASBUzU
Job (FTX) BbNzfRQjTYiCZVfvK1qpQkkon3kP2tbvaCHfzsyjeBU3
✔ Switchboard setup complete
######## Start the Oracle ########
Run the following command in a new shell
ORACLE_KEY=CmTr9FSeuhMPBLEPa3o2M71RwRnBz6LMcsfzHaW721Ak PAYER_KEYPAIR=/Users/switchboard/.config/solana/id.json RPC_URL=https://api.devnet.solana.com docker-compose up
Select 'Y' when the docker container displays Starting listener... [y/n]: y
✔ Crank turned
######## Aggregator Result ########
Result: 30.91
✔ Aggregator succesfully updated!
ts-node src/devnet
```
Optionally, provide these env variables
```bash
RPC_URL=https://my_custom_rpc_url.com \
PAYER_KEYPAIR=~/my_keypair.json \
ts-node src/devnet
```
### Create a Private Queue and Oracle
You can also create your own private Switchboard network and run your own
oracles. This requires you to run your own oracles for this network.
The following script will
- Create a private queue and crank
- Create a new data feed on this network
- Start a local oracle
- Call OpenRound and await the updated result from your local oracle
```bash
ts-node src/private-queue
```
Optionally, provide these env variables
```bash
RPC_URL=https://my_custom_rpc_url.com \
PAYER_KEYPAIR=~/my_keypair.json \
ts-node src/private-queue
```
### Create a Feed with the CLI
First install the sbv2 cli
```bash
npm install -g @switchboard-xyz/cli^2
```
Then run the following command to create your own feed using the devnet
permissionless queue and crank
```bash
export QUEUE_KEY=F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy
export CRANK_KEY=GN9jjCy2THzZxhYqZETmPM3my8vg4R5JyNkgULddUMa5
sbv2 solana aggregator create "$QUEUE_KEY" \
--keypair ~/.config/solana/id.json \
--crankKey "$CRANK_KEY" \
--name "My_Test_Feed" \
--updateInterval 10 \
--minOracles 1 \
--batchSize 1 \
--leaseAmount 0.1 \
--job ./src/oracle-job.json \
--verbose
```
Then request an update for your new feed
```bash
sbv2 solana aggregator update $AGGREGATOR_KEY \
--keypair ~/.config/solana/id.json
```
See
[docs.switchboard.xyz/solana/program/devnet](https://docs.switchboard.xyz/solana/program/devnet)
for a list of devnet accounts to use
**_NOTE:_** You can provide multiple `--job` flags to add additional oracle jobs
to your data feed

View File

@ -12,7 +12,9 @@
"homepage": "https://docs.switchboard.xyz",
"main": "dist/main.js",
"scripts": {
"start": "ts-node src/main",
"start": "ts-node src/private-queue",
"start:devnet": "ts-node src/devnet",
"start:simulate": "ts-node src/simulate",
"build": "rimraf dist && ./esbuild.js -inline-sourcemap",
"test": "echo \"No test script for @switchboard-xyz/v2-feed-walkthrough\" && exit 0"
},

View File

@ -0,0 +1,127 @@
/**
* Create a new data feed on the devnet permissionless queue.
*
* The devnet queue does NOT require you to run your own oracle.
*
* This script will:
* - load the existing devnet permissionless queue
* - load the existing devnet permissionless crank
* - create a new data feed for the queue and crank
* - call open round on the feed and await the result
*/
import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
import { OracleJob } from "@switchboard-xyz/common";
import {
AggregatorAccount,
CrankAccount,
QueueAccount,
SwitchboardProgram,
} from "@switchboard-xyz/solana.js";
import chalk from "chalk";
import dotenv from "dotenv";
import os from "os";
import path from "path";
import { myOracleJob } from "./oracle-job";
import { getKeypair, toAccountString } from "./utils";
dotenv.config();
const DEVNET_PERMISSIONLESS_QUEUE = new PublicKey(
"F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy"
);
const DEVNET_PERMISSIONLESS_CRANK = new PublicKey(
"GN9jjCy2THzZxhYqZETmPM3my8vg4R5JyNkgULddUMa5"
);
async function main() {
// get payer keypair
let payerKeypairPath: string;
if (process.argv.length > 2 && process.argv[2]) {
payerKeypairPath = process.argv[2];
} else if (process.env.PAYER_KEYPAIR) {
payerKeypairPath = process.env.PAYER_KEYPAIR;
} else {
payerKeypairPath = path.join(os.homedir(), ".config/solana/id.json");
}
const authority = getKeypair(payerKeypairPath);
// get RPC_URL
let rpcUrl: string;
if (process.env.RPC_URL) {
rpcUrl = process.env.RPC_URL;
} else {
rpcUrl = clusterApiUrl("devnet");
}
const program = await SwitchboardProgram.load(
"devnet",
new Connection(rpcUrl, "confirmed"),
authority
);
console.log(chalk.yellow("######## Switchboard Setup ########"));
const [queueAccount, queueData] = await QueueAccount.load(
program,
DEVNET_PERMISSIONLESS_QUEUE
);
console.log(toAccountString("Oracle Queue", queueAccount.publicKey));
if (
!queueData.unpermissionedFeedsEnabled &&
!queueData.authority.equals(authority.publicKey)
) {
throw new Error(
`This queue requires the queue authority (${queueData.authority}) to grant you permissions to join`
);
}
const [crankAccount, crankData] = await CrankAccount.load(
program,
DEVNET_PERMISSIONLESS_CRANK
);
console.log(toAccountString("Crank", crankAccount.publicKey));
const [aggregatorAccount] = await queueAccount.createFeed({
name: "SOL_USD",
batchSize: 1,
minRequiredOracleResults: 1,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 10,
fundAmount: 0.1,
enable: false, // permissionless queue does not require explicit permissions
crankPubkey: crankAccount.publicKey,
jobs: [
{
weight: 2,
data: OracleJob.encodeDelimited(myOracleJob).finish(),
},
],
});
console.log(toAccountString("Aggregator", aggregatorAccount.publicKey));
console.log(chalk.green("\u2714 Switchboard setup complete"));
console.log(chalk.yellow("######## Calling OpenRound ########"));
const [newAggregatorState] =
await aggregatorAccount.openRoundAndAwaitResult();
console.log(
`${chalk.blue("Result:")} ${chalk.green(
AggregatorAccount.decodeLatestValue(newAggregatorState).toString()
)}\r\n`
);
console.log(chalk.green("\u2714 Aggregator succesfully updated!"));
}
main().then(
() => {
process.exit();
},
(error) => {
console.error("Failed to create a feed on the devnet permissionless queue");
console.error(error);
process.exit(1);
}
);

View File

@ -0,0 +1,9 @@
{
"tasks": [
{
"valueTask": {
"value": 10
}
}
]
}

View File

@ -0,0 +1,7 @@
import { OracleJob } from "@switchboard-xyz/common";
import fs from "fs";
import path from "path";
export const myOracleJob = OracleJob.fromObject(
fs.readFileSync(path.join(path.dirname(__filename), "oracle-job.json"))
);

View File

@ -1,5 +1,14 @@
import type { PublicKey } from "@solana/web3.js";
import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js";
/**
* Create a private switchboard oracle queue and fulfill your own open round request.
*
* This script will:
* - create a new private switchboard network with a single oracle and crank
* - create a new data feed for the queue and crank
* - start a new switchboard oracle and heartbeat on-chain to signal readiness
* - call open round on the feed and await the result
*/
import { clusterApiUrl, Connection } from "@solana/web3.js";
import { OracleJob, sleep } from "@switchboard-xyz/common";
import { NodeOracle } from "@switchboard-xyz/oracle";
import {
@ -9,41 +18,15 @@ import {
} from "@switchboard-xyz/solana.js";
import chalk from "chalk";
import dotenv from "dotenv";
import fs from "fs";
import os from "os";
import path from "path";
import { myOracleJob } from "./oracle-job";
import { getKeypair, toAccountString } from "./utils";
dotenv.config();
let oracle: NodeOracle | undefined = undefined;
export const toAccountString = (
label: string,
publicKey: PublicKey | string | undefined
): string => {
if (typeof publicKey === "string") {
return `${chalk.blue(label.padEnd(24, " "))} ${chalk.yellow(publicKey)}`;
}
if (!publicKey) {
return "";
}
return `${chalk.blue(label.padEnd(24, " "))} ${chalk.yellow(
publicKey.toString()
)}`;
};
export const getKeypair = (keypairPath: string): Keypair => {
if (!fs.existsSync(keypairPath)) {
throw new Error(
`failed to load authority keypair from ${keypairPath}, try providing a path to your keypair with the script 'ts-node src/main KEYPAIR_PATH'`
);
}
const keypairString = fs.readFileSync(keypairPath, "utf8");
const keypairBuffer = new Uint8Array(JSON.parse(keypairString));
const walletKeypair = Keypair.fromSecretKey(keypairBuffer);
return walletKeypair;
};
async function main() {
// get payer keypair
let payerKeypairPath: string;
@ -124,17 +107,7 @@ async function main() {
jobs: [
{
weight: 2,
data: OracleJob.encodeDelimited(
OracleJob.fromObject({
tasks: [
{
valueTask: {
value: 10,
},
},
],
})
).finish(),
data: OracleJob.encodeDelimited(myOracleJob).finish(),
},
],
});
@ -157,7 +130,15 @@ async function main() {
},
});
await oracle.startAndAwait();
await sleep(1000); // wait 1 extra second for oracle to heartbeat
let retryCount = 5;
while (retryCount) {
if (queueAccount.isReady()) {
retryCount = 0;
break;
}
await sleep(1000);
--retryCount;
}
console.log(chalk.green("\u2714 Oracle ready"));
console.log(chalk.yellow("######## Calling OpenRound ########"));
@ -185,6 +166,6 @@ main().then(
oracle?.stop();
console.error("Failed to create a private feed");
console.error(error);
process.exit(-1);
process.exit(1);
}
);

View File

@ -0,0 +1,26 @@
import { simulateOracleJobs } from "@switchboard-xyz/common";
import chalk from "chalk";
import { myOracleJob } from "./oracle-job";
async function main() {
const response = await simulateOracleJobs([myOracleJob], "devnet");
console.log(
chalk.blue(`\u2139 TaskRunner Version: ${response.taskRunnerVersion}`)
);
console.log(
chalk.green(`\u2714 Result: ${response.result.toString()}`),
`[${response.results.map((r) => r.toString()).join(", ")}]`
);
}
main().then(
() => {
process.exit();
},
(error) => {
console.error("Failed to simulate the oracle job response");
console.error(error);
process.exit(1);
}
);

View File

@ -0,0 +1,30 @@
import { Keypair, PublicKey } from "@solana/web3.js";
import chalk from "chalk";
import fs from "fs";
export const toAccountString = (
label: string,
publicKey: PublicKey | string | undefined
): string => {
if (typeof publicKey === "string") {
return `${chalk.blue(label.padEnd(24, " "))} ${chalk.yellow(publicKey)}`;
}
if (!publicKey) {
return "";
}
return `${chalk.blue(label.padEnd(24, " "))} ${chalk.yellow(
publicKey.toString()
)}`;
};
export const getKeypair = (keypairPath: string): Keypair => {
if (!fs.existsSync(keypairPath)) {
throw new Error(
`failed to load authority keypair from ${keypairPath}, try providing a path to your keypair with the script 'ts-node src/main KEYPAIR_PATH'`
);
}
const keypairString = fs.readFileSync(keypairPath, "utf8");
const keypairBuffer = new Uint8Array(JSON.parse(keypairString));
const walletKeypair = Keypair.fromSecretKey(keypairBuffer);
return walletKeypair;
};

View File

@ -29,5 +29,5 @@
"include": ["src/**/*"],
"exclude": ["esbuild.js", "dist"],
"references": [{ "path": "../switchboard-v2" }],
"files": ["src/main.ts"]
"files": ["src/private-queue.ts"]
}