updated feed-walkthrough example
This commit is contained in:
parent
f84eea9549
commit
9fdfbbe0db
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"tasks": [
|
||||
{
|
||||
"valueTask": {
|
||||
"value": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"))
|
||||
);
|
|
@ -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);
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
};
|
|
@ -29,5 +29,5 @@
|
|||
"include": ["src/**/*"],
|
||||
"exclude": ["esbuild.js", "dist"],
|
||||
"references": [{ "path": "../switchboard-v2" }],
|
||||
"files": ["src/main.ts"]
|
||||
"files": ["src/private-queue.ts"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue