init
This commit is contained in:
commit
0796ca4b4f
|
@ -0,0 +1,4 @@
|
||||||
|
lib
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
scripts
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es2020": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"airbnb-typescript/base",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
// "plugin:unicorn/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": ["./tsconfig.json"]
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint", "prettier", "unicorn", "import"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"@typescript-eslint/no-throw-literal": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unicorn/prefer-node-protocol": "off",
|
||||||
|
"unicorn/prevent-abbreviations": "off",
|
||||||
|
"unicorn/no-await-expression-member": "off",
|
||||||
|
"unicorn/prefer-code-point": "off",
|
||||||
|
"unicorn/no-null": "off",
|
||||||
|
"unicorn/prefer-module": "off",
|
||||||
|
"unicorn/no-array-for-each": "off",
|
||||||
|
"unicorn/prefer-array-some": "off",
|
||||||
|
"unicorn/no-object-as-default-parameter": "off",
|
||||||
|
"unicorn/filename-case": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: Deploy to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
[
|
||||||
|
website/**,
|
||||||
|
".github/workflows/*",
|
||||||
|
"idl/**",
|
||||||
|
"libraries/ts/src/**",
|
||||||
|
"libraries/sbv2-cli/src/**",
|
||||||
|
]
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy to GitHub Pages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
cache: yarn
|
||||||
|
cache-dependency-path: "yarn.lock"
|
||||||
|
- name: Setup npmrc
|
||||||
|
run: echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_AUTH_TOKEN}}" > .npmrc
|
||||||
|
- name: Setup yarnrc
|
||||||
|
run: echo "registry \"https://registry.npmjs.org\"" > .yarnrc
|
||||||
|
- name: Build website
|
||||||
|
run: |
|
||||||
|
yarn install --frozen-lockfile
|
||||||
|
yarn docs:build
|
||||||
|
# Popular action to deploy to GitHub Pages:
|
||||||
|
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Build output to publish to the `gh-pages` branch:
|
||||||
|
publish_dir: ./website/public
|
||||||
|
# Assign commit authorship to the official GH-Actions bot for deploys to `gh-pages` branch:
|
||||||
|
# https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||||
|
# The GH actions bot is used by default if you didn't specify the two fields.
|
||||||
|
# You can swap them out with your own user credentials.
|
||||||
|
user_name: github-actions[bot]
|
||||||
|
user_email: 41898282+github-actions[bot]@users.noreply.github.com
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: Test deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths: [website/**, ".github/workflows/*", "idl/**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-deploy:
|
||||||
|
name: Test deployment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
cache: yarn
|
||||||
|
cache-dependency-path: "yarn.lock"
|
||||||
|
- name: Setup npmrc
|
||||||
|
run: echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_AUTH_TOKEN}}" > .npmrc
|
||||||
|
- name: Setup yarnrc
|
||||||
|
run: echo "registry \"https://registry.npmjs.org\"" > .yarnrc
|
||||||
|
- name: Test build
|
||||||
|
run: |
|
||||||
|
yarn install --frozen-lockfile
|
||||||
|
yarn docs:build
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Javascript
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
build
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
*.env
|
||||||
|
.switchboard
|
||||||
|
start-local-validator.sh
|
||||||
|
start-oracle.sh
|
||||||
|
switchboard.json
|
||||||
|
docker-compose.switchboard.yml
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
programs/**/target
|
||||||
|
Cargo.lock
|
||||||
|
.anchor
|
||||||
|
target
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.keypairs
|
||||||
|
secrets
|
||||||
|
*-keypair*.json
|
||||||
|
.archive
|
||||||
|
|
||||||
|
# Root level jsons ignored
|
||||||
|
job-directory/*.json
|
||||||
|
|
||||||
|
|
||||||
|
# Docusaurus
|
||||||
|
public
|
||||||
|
.docusaurus
|
||||||
|
# Auto generated
|
||||||
|
website/static/api/ts
|
||||||
|
website/static/api/ts-lite
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"files.eol": "\n",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.wordWrap": "on"
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
# switchboard-v2
|
||||||
|
|
||||||
|
A monorepo containing APIs, Utils, and examples for Switchboard V2.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
| ---------------------------------------------- | ------------------------------------------------------------- |
|
||||||
|
| [Typescript](./libraries/ts) | Typescript client to interact with Switchboard V2. |
|
||||||
|
| [Typescript **_Lite_**](./libraries/sbv2-lite) | Typescript "Lite" client to deserialize aggregator accounts |
|
||||||
|
| [Python](./libraries/py) | Python client to interact with Switchboard V2. |
|
||||||
|
| [Rust](./libraries/rs) | Rust client to interact with Switchboard V2. |
|
||||||
|
| [CLI](./cli) | Command Line Interface (CLI) to interact with Switchboard V2. |
|
||||||
|
|
||||||
|
### Program Examples
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [anchor-feed-parser](./programs/anchor-feed-parser) | Anchor example program demonstrating how to deserialize and read an onchain aggregator. |
|
||||||
|
| [spl-feed-parser](./programs/spl-feed-parser) | Solana Program Library example demonstrating how to deserialize and read an onchain aggregator. |
|
||||||
|
| [anchor-vrf-parser](./programs/anchor-vrf-parser) | Anchor example program demonstrating how to deserialize and read an onchain verifiable randomness function (VRF) account. |
|
||||||
|
|
||||||
|
### Client Examples
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [feed-parser](./packages/feed-parser) | Typescript example demonstrating how to read an aggregator account. |
|
||||||
|
| [feed-walkthrough](./packages/feed-walkthrough) | Typescript example demonstrating how to create and manage your own oracle queue. |
|
||||||
|
| [lease-observer](./packages/lease-observer) | Typescript example demonstrating how to send PagerDuty alerts when your aggregator lease is low on funds. |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [Node and Yarn](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||||
|
- [Docker Compose](https://docs.docker.com/compose/install)
|
||||||
|
- [Rust](https://www.rust-lang.org/tools/install)
|
||||||
|
- [Solana](https://docs.solana.com/cli/install-solana-cli-tools)
|
||||||
|
- [Anchor](https://project-serum.github.io/anchor/getting-started/installation.html#install-anchor)
|
||||||
|
- [Python3](https://www.python.org/downloads/)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Typescript Setup
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
yarn workspaces run build
|
||||||
|
yarn workspace @switchboard-xyz/switchboardv2-cli link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Setup
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install poetry
|
||||||
|
cd libraries/py
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn workspaces run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn test:libraries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programs
|
||||||
|
|
||||||
|
```
|
||||||
|
sbv2 localnet:env --keypair ../payer-keypair.json
|
||||||
|
chmod +x ./start-local-validator.sh && chmod +x ./start-oracle.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run each of the commands in a separate shell
|
||||||
|
|
||||||
|
- `./start-local-validator.sh`
|
||||||
|
- `./start-oracle.sh`
|
||||||
|
- `yarn test:anchor`
|
||||||
|
|
||||||
|
## Website
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn workspace website start
|
||||||
|
```
|
|
@ -0,0 +1,6 @@
|
||||||
|
lib
|
||||||
|
dist
|
||||||
|
dts
|
||||||
|
node_modules
|
||||||
|
scripts
|
||||||
|
test
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"oclif",
|
||||||
|
"oclif-typescript",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:unicorn/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint", "prettier", "unicorn", "import"],
|
||||||
|
"rules": {
|
||||||
|
"valid-jsdoc": "off",
|
||||||
|
"camelcase": "off",
|
||||||
|
"max-params": "off",
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/naming-convention": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-shadow": "off",
|
||||||
|
"unicorn/filename-case": "off",
|
||||||
|
"unicorn/import-style": "off",
|
||||||
|
"unicorn/prefer-node-protocol": "off",
|
||||||
|
"unicorn/prefer-code-point": "off",
|
||||||
|
"unicorn/no-await-expression-member": "off",
|
||||||
|
"no-useless-constructor": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"lines-between-class-members": "off",
|
||||||
|
"no-warning-comments": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
*-debug.log
|
||||||
|
*-error.log
|
||||||
|
/.nyc_output
|
||||||
|
/dist
|
||||||
|
/lib
|
||||||
|
/tmp
|
||||||
|
/yarn.lock
|
||||||
|
node_modules
|
||||||
|
switchboardv2_idl*.json
|
||||||
|
.archive
|
||||||
|
.keypairs
|
||||||
|
|
||||||
|
|
||||||
|
*-keypair.json
|
||||||
|
*.schema.json
|
||||||
|
.data
|
||||||
|
|
||||||
|
oclif.manifest.json
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('@oclif/command').run()
|
||||||
|
.then(require('@oclif/command/flush'))
|
||||||
|
.catch(require('@oclif/errors/handle'))
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
node "%~dp0\run" %*
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "WHEAT",
|
||||||
|
"metadata": "",
|
||||||
|
"oracleRequestBatchSize": 1,
|
||||||
|
"minOracleResults": 1,
|
||||||
|
"minJobResults": 1,
|
||||||
|
"minUpdateDelaySeconds": 300,
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"name": "commodities-api WHEAT",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"httpTask": {
|
||||||
|
"url": "https://www.commodities-api.com/api/latest?access_key=ke9lwg53l34qis22zr2t568f8k32agaewndr3j8mvzr33ys9wixhrudh73fj&base=USD&symbols=WHEAT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonParseTask": {
|
||||||
|
"path": "$.data.rates.WHEAT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"powTask": {
|
||||||
|
"scalar": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "FtxCom MNGO/USD",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"httpTask": {
|
||||||
|
"url": "https://ftx.com/api/markets/mngo/usd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonParseTask": {
|
||||||
|
"path": "$.result.price"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "Serum SOL/USDC",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"serumSwapTask": {
|
||||||
|
"serumPoolAddress": "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"name": "test_1",
|
||||||
|
"metadata": "queue test_1",
|
||||||
|
"minStake": 0,
|
||||||
|
"minUpdateDelaySeconds": 10,
|
||||||
|
"cranks": [
|
||||||
|
{
|
||||||
|
"name": "crank-1",
|
||||||
|
"maxRows": 125
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "crank-2",
|
||||||
|
"maxRows": 150
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"oracles": 1,
|
||||||
|
"aggregators": [
|
||||||
|
{
|
||||||
|
"name": "MNGO_USD",
|
||||||
|
"metadata": "",
|
||||||
|
"crank": "crank-1",
|
||||||
|
"oracleRequestBatchSize": 1,
|
||||||
|
"minOracleResults": 1,
|
||||||
|
"minJobResults": 1,
|
||||||
|
"minUpdateDelaySeconds": 6,
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"name": "FtxCom MNGO/USD",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"httpTask": {
|
||||||
|
"url": "https://ftx.com/api/markets/mngo/usd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonParseTask": {
|
||||||
|
"path": "$.result.price"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Raydium MNGO/USDC",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"httpTask": {
|
||||||
|
"url": "https://api.raydium.io/pairs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonParseTask": {
|
||||||
|
"path": "$[?(@.name == 'MNGO-USDC')].price"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
{
|
||||||
|
"name": "@switchboard-xyz/switchboardv2-cli",
|
||||||
|
"description": "command line tool to interact with switchboard v2",
|
||||||
|
"version": "0.1.18",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/switchboard-xyz/switchboard-v2",
|
||||||
|
"directory": "cli"
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/switchboard-xyz/switchboard-v2/tree/main/cli/issues",
|
||||||
|
"homepage": "https://docs.switchboard.xyz",
|
||||||
|
"bin": {
|
||||||
|
"sbv2": "./bin/run"
|
||||||
|
},
|
||||||
|
"oclif": {
|
||||||
|
"commands": "./lib/commands",
|
||||||
|
"bin": "sbv2",
|
||||||
|
"dirname": "@switchboard-xyz/sbv2-cli",
|
||||||
|
"macos": {
|
||||||
|
"identifier": "com.sbv2.cli"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"s3": {
|
||||||
|
"bucket": "sbv2-cli",
|
||||||
|
"templates": {
|
||||||
|
"target": {
|
||||||
|
"unversioned": "tarballs/<%- bin %>/<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-<%- platform %>-<%- arch %><%- ext %>",
|
||||||
|
"versioned": "tarballs/<%- bin %>/<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>",
|
||||||
|
"manifest": "tarballs/<%- bin %>/<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- platform %>-<%- arch %>"
|
||||||
|
},
|
||||||
|
"vanilla": {
|
||||||
|
"unversioned": "tarballs/<%- bin %>/<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %><%- ext %>",
|
||||||
|
"versioned": "tarballs/<%- bin %>/<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %><%- bin %>-v<%- version %>/<%- bin %>-v<%- version %><%- ext %>",
|
||||||
|
"manifest": "tarballs/<%- bin %>/<%- channel === 'stable' ? '' : 'channels/' + channel + '/' %>version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"version": "16.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@oclif/plugin-update",
|
||||||
|
"@oclif/plugin-help",
|
||||||
|
"@oclif/plugin-warn-if-update-available",
|
||||||
|
"@oclif/config"
|
||||||
|
],
|
||||||
|
"topics": {
|
||||||
|
"aggregator": {
|
||||||
|
"description": "interact with a switchboard aggregator account"
|
||||||
|
},
|
||||||
|
"lease": {
|
||||||
|
"description": "interact with a switchboard lease account"
|
||||||
|
},
|
||||||
|
"crank": {
|
||||||
|
"description": "interact with a switchboard crank account"
|
||||||
|
},
|
||||||
|
"queue": {
|
||||||
|
"description": "interact with a switchboard oracle queue account"
|
||||||
|
},
|
||||||
|
"job": {
|
||||||
|
"description": "interact with a switchboard job account"
|
||||||
|
},
|
||||||
|
"oracle": {
|
||||||
|
"description": "interact with a switchboard oracle account"
|
||||||
|
},
|
||||||
|
"print": {
|
||||||
|
"description": "find and print a switchboard account by public key for a given cluster"
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"description": "create and manage an oracle queue from a json file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postpack": "rm -f oclif.manifest.json",
|
||||||
|
"posttest1": "eslint . --ext .ts --config .eslintrc.json",
|
||||||
|
"prepack": "npm run build",
|
||||||
|
"build": "rimraf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
|
||||||
|
"doc": "oclif-dev readme",
|
||||||
|
"doc:out": "oclif-dev readme --multi --dir ../website/api/switchboardv2-cli/ && cd ../website/api/switchboardv2-cli/ && for f in *.md; do mv \"${f%.md}.md\" \"_${f%.md}.md\"; sed -i \"\" '1,2d' \"_${f%.md}.md\"; done",
|
||||||
|
"test:old": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
|
||||||
|
"test": "echo \"No test script for @switchboard-xyz/switchboardv2-cli\" && exit 0",
|
||||||
|
"version": "oclif-dev readme && git add README.md",
|
||||||
|
"fmt": "prettier --write 'src/**/*.ts'",
|
||||||
|
"lint": "eslint . --ext .ts --config .eslintrc.json --fix",
|
||||||
|
"postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag \"sbv2-cli/v$PACKAGE_VERSION\" && git push --tags"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google-cloud/secret-manager": "^3.10.1",
|
||||||
|
"@oclif/command": "^1.8.16",
|
||||||
|
"@oclif/config": "^1.18.2",
|
||||||
|
"@oclif/parser": "^3.8.6",
|
||||||
|
"@oclif/plugin-autocomplete": "^1.2.0",
|
||||||
|
"@oclif/plugin-help": "^5.1.12",
|
||||||
|
"@oclif/plugin-update": "^1.5.0",
|
||||||
|
"@oclif/plugin-warn-if-update-available": "^1.7.3",
|
||||||
|
"@project-serum/anchor": "^0.24.2",
|
||||||
|
"@solana/spl-token": "^0.1.8",
|
||||||
|
"@solana/web3.js": "^1.41.10",
|
||||||
|
"@switchboard-xyz/sbv2-utils": "^0.0.9",
|
||||||
|
"@switchboard-xyz/switchboard-v2": "0.0.97",
|
||||||
|
"assert": "^2.0.0",
|
||||||
|
"big.js": "^6.1.1",
|
||||||
|
"bs58": "^5.0.0",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"decimal.js": "^10.3.1",
|
||||||
|
"node-fetch": "^2.6.6",
|
||||||
|
"readline-sync": "^1.4.10",
|
||||||
|
"winston": "^3.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@oclif/dev-cli": "^1.26.5",
|
||||||
|
"@oclif/test": "^2.0.3",
|
||||||
|
"@types/mocha": "^5.2.7",
|
||||||
|
"@types/node": "^17.0.31",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||||
|
"@typescript-eslint/parser": "^5.5.0",
|
||||||
|
"aws-sdk": "^2.1116.0",
|
||||||
|
"eslint": "^8.3.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
|
"eslint-config-oclif": "^3.1.0",
|
||||||
|
"eslint-config-oclif-typescript": "^1.0.2",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-import": "^2.25.3",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-unicorn": "^39.0.0",
|
||||||
|
"mocha": "^9.1.3",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
|
"prettier": "^2.5.0",
|
||||||
|
"prettier-plugin-organize-imports": "^2.3.4",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-node": "^10.4.0",
|
||||||
|
"tslib": "^2.3.1",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.5.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"/bin",
|
||||||
|
"/lib",
|
||||||
|
"./src",
|
||||||
|
"/examples",
|
||||||
|
"/npm-shrinkwrap.json",
|
||||||
|
"/oclif.manifest.json"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"oclif",
|
||||||
|
"switchboard",
|
||||||
|
"solana",
|
||||||
|
"oracle"
|
||||||
|
],
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts"
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
queue="F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy"
|
||||||
|
|
||||||
|
authority=$1
|
||||||
|
if [[ -z "${authority}" ]]; then
|
||||||
|
read -rp "Enter the path to the authority keypair: " authority
|
||||||
|
fi
|
||||||
|
echo -e "authority: $(solana-keygen pubkey "$authority")"
|
||||||
|
|
||||||
|
# TODO: Transfer required balance to fresh keypair for test
|
||||||
|
|
||||||
|
# Create Oracle
|
||||||
|
oracle=$(sbv2 oracle:create $queue --keypair "$authority" --silent)
|
||||||
|
echo -e "oracle: $oracle"
|
||||||
|
|
||||||
|
# TODO: Airdrop and wrap 1 SOL
|
||||||
|
|
||||||
|
# TODO: Deposit into oracle staking wallet
|
||||||
|
|
||||||
|
# TODO: Check oracle balance matches deposit
|
||||||
|
|
||||||
|
# TODO: Withdraw from oracle staking wallet
|
|
@ -0,0 +1,290 @@
|
||||||
|
/* eslint-disable unicorn/no-process-exit */
|
||||||
|
/* eslint-disable no-process-exit */
|
||||||
|
import Command, { flags } from "@oclif/command";
|
||||||
|
import { Input } from "@oclif/parser";
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import {
|
||||||
|
Cluster,
|
||||||
|
clusterApiUrl,
|
||||||
|
Connection,
|
||||||
|
Keypair,
|
||||||
|
PublicKey,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import { BigUtils } from "@switchboard-xyz/sbv2-utils";
|
||||||
|
import {
|
||||||
|
getSwitchboardPid,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import Big from "big.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { DEFAULT_KEYPAIR } from "./accounts";
|
||||||
|
import { CliConfig, ConfigParameter, DEFAULT_CONFIG } from "./config";
|
||||||
|
import { AuthorityMismatch } from "./types";
|
||||||
|
import { CommandContext } from "./types/context/context";
|
||||||
|
import { FsProvider } from "./types/context/FsProvider";
|
||||||
|
import { LoggerParameters, LogProvider } from "./types/context/logging";
|
||||||
|
import { FAILED_ICON, loadKeypair, toCluster } from "./utils";
|
||||||
|
|
||||||
|
abstract class BaseCommand extends Command {
|
||||||
|
static flags = {
|
||||||
|
help: flags.help({ char: "h" }),
|
||||||
|
verbose: flags.boolean({
|
||||||
|
char: "v",
|
||||||
|
description: "log everything",
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
silent: flags.boolean({
|
||||||
|
char: "s",
|
||||||
|
description: "suppress cli prompts",
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
mainnetBeta: flags.boolean({
|
||||||
|
description: "WARNING: use mainnet-beta solana cluster",
|
||||||
|
}),
|
||||||
|
rpcUrl: flags.string({
|
||||||
|
char: "u",
|
||||||
|
description: "alternate RPC url",
|
||||||
|
}),
|
||||||
|
programId: flags.string({
|
||||||
|
description: "alternative Switchboard program ID to interact with",
|
||||||
|
}),
|
||||||
|
keypair: flags.string({
|
||||||
|
char: "k",
|
||||||
|
description:
|
||||||
|
"keypair that will pay for onchain transactions. defaults to new account authority if no alternate authority provided",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public silent: boolean; // TODO: move to logger
|
||||||
|
|
||||||
|
public verbose: boolean; // TODO: move to logger
|
||||||
|
|
||||||
|
public cluster: Cluster;
|
||||||
|
|
||||||
|
public connection: Connection;
|
||||||
|
|
||||||
|
public cliConfig: CliConfig;
|
||||||
|
|
||||||
|
public logger: LogProvider;
|
||||||
|
|
||||||
|
public context: CommandContext;
|
||||||
|
|
||||||
|
public program: anchor.Program;
|
||||||
|
|
||||||
|
public payerKeypair?: Keypair | undefined = undefined;
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const { flags } = this.parse(<Input<any>>this.constructor);
|
||||||
|
BaseCommand.flags = flags;
|
||||||
|
|
||||||
|
// setup logging
|
||||||
|
this.silent = flags.silent;
|
||||||
|
this.verbose = flags.verbose;
|
||||||
|
const level = flags.silent ? "error" : flags.verbose ? "debug" : "info";
|
||||||
|
const logFilename = path.join(this.config.cacheDir, "log.txt");
|
||||||
|
const logParameters: LoggerParameters = {
|
||||||
|
console: {
|
||||||
|
level,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
level: "debug",
|
||||||
|
filename: logFilename,
|
||||||
|
},
|
||||||
|
silent: flags.silent,
|
||||||
|
verbose: flags.verbose,
|
||||||
|
};
|
||||||
|
this.logger = new LogProvider(logParameters);
|
||||||
|
|
||||||
|
fs.mkdirSync(this.config.dataDir, { recursive: true });
|
||||||
|
|
||||||
|
this.loadConfig();
|
||||||
|
|
||||||
|
this.cluster = flags.mainnetBeta
|
||||||
|
? toCluster("mainnet-beta")
|
||||||
|
: toCluster("devnet");
|
||||||
|
const url = flags.rpcUrl ?? clusterApiUrl(this.cluster);
|
||||||
|
try {
|
||||||
|
this.connection = new Connection(url, {
|
||||||
|
commitment: "finalized",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
this.connection = new Connection(clusterApiUrl(this.cluster), {
|
||||||
|
commitment: "finalized",
|
||||||
|
});
|
||||||
|
this.logger.log(
|
||||||
|
`resetting rpc url for ${this.cluster}. invalid URL ${url}`
|
||||||
|
);
|
||||||
|
this.setConfig(
|
||||||
|
this.cluster === "devnet" ? "devnet-rpc" : "mainnet-rpc",
|
||||||
|
clusterApiUrl(this.cluster)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.payerKeypair = flags.keypair
|
||||||
|
? await loadKeypair(flags.keypair)
|
||||||
|
: DEFAULT_KEYPAIR;
|
||||||
|
|
||||||
|
const programId = flags.programId
|
||||||
|
? new anchor.web3.PublicKey(flags.programId)
|
||||||
|
: getSwitchboardPid(this.cluster as "mainnet-beta" | "devnet");
|
||||||
|
|
||||||
|
const wallet = new anchor.Wallet(this.payerKeypair);
|
||||||
|
const provider = new anchor.AnchorProvider(this.connection, wallet, {
|
||||||
|
commitment: "finalized",
|
||||||
|
// preflightCommitment: "finalized",
|
||||||
|
});
|
||||||
|
|
||||||
|
const anchorIdl = await anchor.Program.fetchIdl(programId, provider);
|
||||||
|
if (!anchorIdl) {
|
||||||
|
throw new Error(`failed to read idl for ${programId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.program = new anchor.Program(anchorIdl, programId, provider);
|
||||||
|
|
||||||
|
if (this.verbose) {
|
||||||
|
this.logger.log("verbose logging enabled");
|
||||||
|
}
|
||||||
|
this.logger.debug(chalk.underline(chalk.blue("## Config".padEnd(16))));
|
||||||
|
this.logger.debug(
|
||||||
|
`${chalk.yellow("cluster:")} ${chalk.blue(this.cluster)}`
|
||||||
|
);
|
||||||
|
this.logger.debug(`${chalk.yellow("rpc:")} ${chalk.blue(url)}`);
|
||||||
|
|
||||||
|
this.context = {
|
||||||
|
logger: this.logger,
|
||||||
|
fs: new FsProvider(this.config.dataDir, this.logger),
|
||||||
|
config: this.cliConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error, message?: string) {
|
||||||
|
// fall back to console if logger is not initialized yet
|
||||||
|
const logger = this.logger ?? console;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
logger.info(chalk.red(`${FAILED_ICON}${message}`));
|
||||||
|
}
|
||||||
|
if (error.message) {
|
||||||
|
const messageLines = error.message.split("\n");
|
||||||
|
logger.error(messageLines[0]);
|
||||||
|
}
|
||||||
|
if (this.verbose) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
// if (error.stack) {
|
||||||
|
// logger.error(error);
|
||||||
|
// } else {
|
||||||
|
// logger.error(error.toString());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.exit(1); // causes unreadable errors?
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load an authority from a CLI flag and optionally check if it matches the expected account authority */
|
||||||
|
async loadAuthority(
|
||||||
|
authorityPath?: string,
|
||||||
|
expectedAuthority?: PublicKey
|
||||||
|
): Promise<Keypair> {
|
||||||
|
const authority = authorityPath
|
||||||
|
? await loadKeypair(authorityPath)
|
||||||
|
: programWallet(this.program);
|
||||||
|
|
||||||
|
if (expectedAuthority && !expectedAuthority.equals(authority.publicKey)) {
|
||||||
|
throw new AuthorityMismatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
return authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainnetCheck(): void {
|
||||||
|
if (this.cluster === "mainnet-beta") {
|
||||||
|
throw new Error(
|
||||||
|
`switchboardv2-cli is still in beta, mainnet is disabled for this command.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadConfig(): void {
|
||||||
|
const configPath = path.join(this.config.configDir, "config.json");
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const userConfig: CliConfig = JSON.parse(
|
||||||
|
fs.readFileSync(configPath, "utf-8")
|
||||||
|
);
|
||||||
|
this.cliConfig = userConfig;
|
||||||
|
} else {
|
||||||
|
fs.mkdirSync(this.config.configDir, { recursive: true });
|
||||||
|
this.saveConfig(DEFAULT_CONFIG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig(config: CliConfig): void {
|
||||||
|
this.cliConfig = config;
|
||||||
|
const configPath = path.join(this.config.configDir, "config.json");
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, undefined, 2));
|
||||||
|
this.logger.info(chalk.green("Saved Config: ") + configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(parameter: ConfigParameter, value?: string) {
|
||||||
|
switch (parameter) {
|
||||||
|
case "devnet-rpc": {
|
||||||
|
const newConfig = {
|
||||||
|
...this.cliConfig,
|
||||||
|
devnet: {
|
||||||
|
...this.cliConfig.devnet,
|
||||||
|
rpcUrl: value || clusterApiUrl(toCluster("devnet")),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.saveConfig(newConfig);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "mainnet-rpc": {
|
||||||
|
const newConfig = {
|
||||||
|
...this.cliConfig,
|
||||||
|
mainnet: {
|
||||||
|
...this.cliConfig.devnet,
|
||||||
|
rpcUrl: value || clusterApiUrl(toCluster("mainnet-beta")),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.saveConfig(newConfig);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
this.logger.warn("not implemented yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRpcUrl(cluster: Cluster): string {
|
||||||
|
switch (cluster) {
|
||||||
|
case "devnet":
|
||||||
|
return (
|
||||||
|
this.cliConfig.devnet.rpcUrl || clusterApiUrl(toCluster("devnet"))
|
||||||
|
);
|
||||||
|
case "mainnet-beta":
|
||||||
|
return (
|
||||||
|
this.cliConfig.devnet.rpcUrl ||
|
||||||
|
clusterApiUrl(toCluster("mainnet-beta"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a string to a tokenAmount
|
||||||
|
// If a decimal is found, it will be normalized using 9 decimal places
|
||||||
|
getTokenAmount(value: string, decimals = 9): anchor.BN {
|
||||||
|
if (isNaN(Number(value))) {
|
||||||
|
throw new Error("tokenAmount must be an integer or decimal");
|
||||||
|
}
|
||||||
|
if (value.split(".").length > 1) {
|
||||||
|
const float = new Big(value);
|
||||||
|
const scale = BigUtils.safePow(new Big(10), decimals);
|
||||||
|
const tokenAmount = BigUtils.safeMul(float, scale);
|
||||||
|
return new anchor.BN(tokenAmount.toFixed(0));
|
||||||
|
}
|
||||||
|
return new anchor.BN(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseCommand;
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { Input } from "@oclif/parser";
|
||||||
|
import { Keypair } from "@solana/web3.js";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import {
|
||||||
|
OracleQueueClass,
|
||||||
|
pubKeyConverter,
|
||||||
|
pubKeyReviver,
|
||||||
|
QueueDefinition,
|
||||||
|
} from "./accounts";
|
||||||
|
import BaseCommand from "./BaseCommand";
|
||||||
|
import { loadKeypair } from "./utils";
|
||||||
|
|
||||||
|
abstract class JsonBaseCommand extends BaseCommand {
|
||||||
|
public queueAuthority?: Keypair;
|
||||||
|
|
||||||
|
public queueSchemaPath?: string;
|
||||||
|
|
||||||
|
public queueSchema?: QueueDefinition;
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description:
|
||||||
|
"alternate keypair that is the authority for the oracle queue",
|
||||||
|
}),
|
||||||
|
schema: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "filesystem path for an oracle queue schema",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await super.init();
|
||||||
|
const { flags } = this.parse(<Input<any>>this.constructor);
|
||||||
|
JsonBaseCommand.flags = flags;
|
||||||
|
|
||||||
|
this.queueSchemaPath =
|
||||||
|
flags.schema && flags.schema.startsWith("/")
|
||||||
|
? flags.schema
|
||||||
|
: path.join(process.cwd(), flags.schema);
|
||||||
|
|
||||||
|
if (fs.existsSync(this.queueSchemaPath)) {
|
||||||
|
this.queueSchema = JSON.parse(
|
||||||
|
fs.readFileSync(this.queueSchemaPath, "utf-8"),
|
||||||
|
pubKeyReviver
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.authority) {
|
||||||
|
this.queueAuthority = flags.authority
|
||||||
|
? await loadKeypair(flags.authority)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(queue: OracleQueueClass) {
|
||||||
|
const outputString = JSON.stringify(queue, pubKeyConverter, 2);
|
||||||
|
if (!outputString || outputString.length === 0) {
|
||||||
|
throw new Error(`failed to save oracle queue (len === 0)`);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(this.queueSchemaPath, outputString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JsonBaseCommand;
|
|
@ -0,0 +1,244 @@
|
||||||
|
/* eslint-disable unicorn/no-process-exit */
|
||||||
|
/* eslint-disable no-process-exit */
|
||||||
|
import Command, { flags } from "@oclif/command";
|
||||||
|
import { Input } from "@oclif/parser";
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { ACCOUNT_DISCRIMINATOR_SIZE } from "@project-serum/anchor/dist/cjs/coder";
|
||||||
|
import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
prettyPrintAggregator,
|
||||||
|
prettyPrintCrank,
|
||||||
|
prettyPrintJob,
|
||||||
|
prettyPrintLease,
|
||||||
|
prettyPrintOracle,
|
||||||
|
prettyPrintPermissions,
|
||||||
|
prettyPrintProgramState,
|
||||||
|
prettyPrintQueue,
|
||||||
|
prettyPrintVrf,
|
||||||
|
} from "@switchboard-xyz/sbv2-utils";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
CrankAccount,
|
||||||
|
JobAccount,
|
||||||
|
LeaseAccount,
|
||||||
|
OracleAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
PermissionAccount,
|
||||||
|
ProgramStateAccount,
|
||||||
|
VrfAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import * as path from "path";
|
||||||
|
import {
|
||||||
|
DEFAULT_KEYPAIR,
|
||||||
|
SwitchboardAccountType,
|
||||||
|
SWITCHBOARD_DISCRIMINATOR_MAP,
|
||||||
|
} from "./accounts";
|
||||||
|
import { CliConfig } from "./config";
|
||||||
|
import { FsProvider } from "./types";
|
||||||
|
import { CommandContext } from "./types/context/context";
|
||||||
|
import { LoggerParameters, LogProvider } from "./types/context/logging";
|
||||||
|
import { FAILED_ICON, loadAnchor } from "./utils";
|
||||||
|
|
||||||
|
export interface ClusterConfigs {
|
||||||
|
devnet: anchor.Program;
|
||||||
|
mainnet: anchor.Program;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class PrintBaseCommand extends Command {
|
||||||
|
static flags = {
|
||||||
|
help: flags.help({ char: "h" }),
|
||||||
|
verbose: flags.boolean({
|
||||||
|
char: "v",
|
||||||
|
description: "log everything",
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public cliConfig: CliConfig;
|
||||||
|
|
||||||
|
public logger: LogProvider;
|
||||||
|
|
||||||
|
public context: CommandContext;
|
||||||
|
|
||||||
|
public clusters: ClusterConfigs;
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const { flags } = this.parse(<Input<any>>this.constructor);
|
||||||
|
PrintBaseCommand.flags = flags;
|
||||||
|
|
||||||
|
// setup logging
|
||||||
|
const level = flags.silent ? "error" : flags.verbose ? "debug" : "info";
|
||||||
|
const logFilename = path.join(this.config.cacheDir, "log.txt");
|
||||||
|
const logParameters: LoggerParameters = {
|
||||||
|
console: {
|
||||||
|
level,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
level: "debug",
|
||||||
|
filename: logFilename,
|
||||||
|
},
|
||||||
|
silent: flags.silent,
|
||||||
|
verbose: flags.verbose,
|
||||||
|
};
|
||||||
|
this.logger = new LogProvider(logParameters);
|
||||||
|
|
||||||
|
this.context = {
|
||||||
|
logger: this.logger,
|
||||||
|
fs: new FsProvider(this.config.dataDir, this.logger),
|
||||||
|
config: this.cliConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clusters = {
|
||||||
|
devnet: await loadAnchor(
|
||||||
|
"devnet",
|
||||||
|
new Connection(clusterApiUrl("devnet")),
|
||||||
|
DEFAULT_KEYPAIR
|
||||||
|
),
|
||||||
|
mainnet: await loadAnchor(
|
||||||
|
"mainnet-beta",
|
||||||
|
new Connection(clusterApiUrl("mainnet-beta")),
|
||||||
|
DEFAULT_KEYPAIR
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async printAccount(
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey,
|
||||||
|
accountType: SwitchboardAccountType
|
||||||
|
) {
|
||||||
|
switch (accountType) {
|
||||||
|
case "JobAccountData": {
|
||||||
|
const job = new JobAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintJob(job));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "AggregatorAccountData": {
|
||||||
|
const aggregator = new AggregatorAccount({ program, publicKey });
|
||||||
|
this.logger.log(
|
||||||
|
await prettyPrintAggregator(aggregator, undefined, true, true)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "OracleAccountData": {
|
||||||
|
const oracle = new OracleAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintOracle(oracle, undefined, true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "PermissionAccountData": {
|
||||||
|
const permission = new PermissionAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintPermissions(permission, undefined));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "LeaseAccountData": {
|
||||||
|
const lease = new LeaseAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintLease(lease, undefined));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "OracleQueueAccountData": {
|
||||||
|
const queue = new OracleQueueAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintQueue(queue));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "CrankAccountData": {
|
||||||
|
const crank = new CrankAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintCrank(crank));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SbState":
|
||||||
|
case "ProgramStateAccountData": {
|
||||||
|
const [programState] = ProgramStateAccount.fromSeed(program);
|
||||||
|
this.logger.log(await prettyPrintProgramState(programState));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "VrfAccountData": {
|
||||||
|
const vrfAccount = new VrfAccount({ program, publicKey });
|
||||||
|
this.logger.log(await prettyPrintVrf(vrfAccount));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "BUFFERxx": {
|
||||||
|
console.log(`Found buffer account but dont know which one`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should also check if pubkey is a token account
|
||||||
|
async printDevnetAccount(
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<SwitchboardAccountType> {
|
||||||
|
const account =
|
||||||
|
await this.clusters.devnet.provider.connection.getAccountInfo(publicKey);
|
||||||
|
if (!account) {
|
||||||
|
throw new Error(`devnet account not found`);
|
||||||
|
}
|
||||||
|
const accountDiscriminator = account.data.slice(
|
||||||
|
0,
|
||||||
|
ACCOUNT_DISCRIMINATOR_SIZE
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log(`[${Uint8Array.from(accountDiscriminator)}]`);
|
||||||
|
|
||||||
|
for await (const [
|
||||||
|
accountType,
|
||||||
|
discriminator,
|
||||||
|
] of SWITCHBOARD_DISCRIMINATOR_MAP.entries()) {
|
||||||
|
if (Buffer.compare(accountDiscriminator, discriminator) === 0) {
|
||||||
|
await this.printAccount(this.clusters.devnet, publicKey, accountType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`no devnet switchboard account found for ${publicKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// should also check if pubkey is a token account
|
||||||
|
async printMainnetAccount(
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<SwitchboardAccountType> {
|
||||||
|
const account =
|
||||||
|
await this.clusters.mainnet.provider.connection.getAccountInfo(publicKey);
|
||||||
|
if (!account) {
|
||||||
|
throw new Error(`mainnet account not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountDiscriminator = account.data.slice(
|
||||||
|
0,
|
||||||
|
ACCOUNT_DISCRIMINATOR_SIZE
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const [
|
||||||
|
accountType,
|
||||||
|
discriminator,
|
||||||
|
] of SWITCHBOARD_DISCRIMINATOR_MAP.entries()) {
|
||||||
|
if (Buffer.compare(accountDiscriminator, discriminator) === 0) {
|
||||||
|
await this.printAccount(this.clusters.mainnet, publicKey, accountType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`no mainnet switchboard account found for ${publicKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error, message?: string) {
|
||||||
|
// fall back to console if logger is not initialized yet
|
||||||
|
const logger = this.logger ?? console;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
logger.info(chalk.red(`${FAILED_ICON}${message}`));
|
||||||
|
}
|
||||||
|
if (error.message) {
|
||||||
|
const messageLines = error.message.split("\n");
|
||||||
|
logger.error(messageLines[0]);
|
||||||
|
} else if (error.stack) {
|
||||||
|
logger.error(error);
|
||||||
|
} else {
|
||||||
|
logger.error(error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exit(1); // causes unreadable errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrintBaseCommand;
|
|
@ -0,0 +1,600 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
SwitchboardDecimal,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import Big from "big.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { ProgramStateClass } from "..";
|
||||||
|
import { AggregatorIllegalRoundOpenCall } from "../../types";
|
||||||
|
import {
|
||||||
|
CommandContext,
|
||||||
|
DEFAULT_CONTEXT,
|
||||||
|
LogProvider,
|
||||||
|
} from "../../types/context";
|
||||||
|
import { getProgramPayer } from "../../utils";
|
||||||
|
import { JobClass, JobDefinition } from "../job";
|
||||||
|
import { LeaseClass } from "../lease";
|
||||||
|
import { PermissionClass } from "../permission";
|
||||||
|
import { copyAccount, DEFAULT_PUBKEY } from "../types";
|
||||||
|
import {
|
||||||
|
anchorBNtoDateTimeString,
|
||||||
|
buffer2string,
|
||||||
|
chalkString,
|
||||||
|
pubKeyConverter,
|
||||||
|
} from "../utils";
|
||||||
|
import {
|
||||||
|
AggregatorAccountData,
|
||||||
|
AggregatorDefinition,
|
||||||
|
fromAggregatorJSON,
|
||||||
|
IAggregatorClass,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class AggregatorClass implements IAggregatorClass {
|
||||||
|
account: AggregatorAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
authorWalletPublicKey: PublicKey;
|
||||||
|
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
|
||||||
|
oracleRequestBatchSize: number; // REQ, will default to some value
|
||||||
|
|
||||||
|
crankPublicKey?: PublicKey;
|
||||||
|
|
||||||
|
historyBufferPublicKey?: PublicKey;
|
||||||
|
|
||||||
|
expiration: anchor.BN;
|
||||||
|
|
||||||
|
forceReportPeriod: anchor.BN;
|
||||||
|
|
||||||
|
isLocked?: boolean;
|
||||||
|
|
||||||
|
metadata: string;
|
||||||
|
|
||||||
|
minRequiredJobResults: number; // REQ, will default to 75% of jobs
|
||||||
|
|
||||||
|
minRequiredOracleResults: number; // REQ, will default to 1
|
||||||
|
|
||||||
|
minUpdateDelaySeconds: number; // REQ, will default to 30s
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
|
||||||
|
startAfter: number;
|
||||||
|
|
||||||
|
varianceThreshold: SwitchboardDecimal;
|
||||||
|
|
||||||
|
jobs: JobClass[];
|
||||||
|
|
||||||
|
leaseAccount: LeaseClass;
|
||||||
|
|
||||||
|
permissionAccount: PermissionClass;
|
||||||
|
|
||||||
|
result: string;
|
||||||
|
|
||||||
|
resultTimestamp: string;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static async init(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregatorAccount: AggregatorAccount,
|
||||||
|
definition?: AggregatorDefinition
|
||||||
|
) {
|
||||||
|
const aggregator = new AggregatorClass();
|
||||||
|
const wallet = programWallet(aggregatorAccount.program);
|
||||||
|
aggregator.account = aggregatorAccount;
|
||||||
|
aggregator.publicKey = aggregator.account.publicKey;
|
||||||
|
aggregator.logger = context.logger;
|
||||||
|
|
||||||
|
await aggregator.loadData();
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: aggregatorAccount.program,
|
||||||
|
publicKey: aggregator.queuePublicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
aggregator.permissionAccount = await PermissionClass.build(
|
||||||
|
context,
|
||||||
|
aggregator.account,
|
||||||
|
queueAccount,
|
||||||
|
definition && "permissionAccount" in definition
|
||||||
|
? definition.permissionAccount
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
aggregator.leaseAccount = await LeaseClass.build(
|
||||||
|
context,
|
||||||
|
aggregator.account,
|
||||||
|
queueAccount,
|
||||||
|
definition && "leaseAccount" in definition
|
||||||
|
? definition.leaseAccount
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { authority } = await queueAccount.loadData();
|
||||||
|
if (
|
||||||
|
aggregator.permissionAccount.permission === "NONE" &&
|
||||||
|
authority.equals(wallet.publicKey)
|
||||||
|
) {
|
||||||
|
aggregator.permissionAccount = await PermissionClass.grantPermission(
|
||||||
|
context,
|
||||||
|
aggregator.account,
|
||||||
|
wallet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
await aggregator.loadData();
|
||||||
|
|
||||||
|
return aggregator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async build(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: AggregatorDefinition,
|
||||||
|
queueAccount?: OracleQueueAccount
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
"account" in definition &&
|
||||||
|
definition.account instanceof AggregatorAccount
|
||||||
|
) {
|
||||||
|
return AggregatorClass.init(context, definition.account, definition);
|
||||||
|
}
|
||||||
|
if ("publicKey" in definition) {
|
||||||
|
return AggregatorClass.fromPublicKey(
|
||||||
|
context,
|
||||||
|
program,
|
||||||
|
definition.publicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (queueAccount) {
|
||||||
|
// need queue account defined to create any new aggregators
|
||||||
|
if ("jobs" in definition) {
|
||||||
|
if (definition.jobs.length > 0) {
|
||||||
|
return AggregatorClass.fromJSON(context, queueAccount, definition);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
"need to provide at least one job definition to build an aggregator"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("sourcePublicKey" in definition) {
|
||||||
|
return AggregatorClass.fromCopyAccount(
|
||||||
|
context,
|
||||||
|
queueAccount,
|
||||||
|
definition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`failed to build aggregator from definition ${definition}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
account: AggregatorAccount
|
||||||
|
) {
|
||||||
|
return AggregatorClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
) {
|
||||||
|
const account = new AggregatorAccount({
|
||||||
|
program,
|
||||||
|
publicKey,
|
||||||
|
});
|
||||||
|
return AggregatorClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromJSON(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
definition: fromAggregatorJSON
|
||||||
|
) {
|
||||||
|
if (!definition.jobs || definition.jobs.length === 0)
|
||||||
|
throw new Error("cannot create an aggregator with no jobs provided");
|
||||||
|
const aggregatorAccount = await AggregatorAccount.create(
|
||||||
|
queueAccount.program,
|
||||||
|
{
|
||||||
|
authority:
|
||||||
|
definition.authorityPublicKey ??
|
||||||
|
getProgramPayer(queueAccount.program).publicKey,
|
||||||
|
authorWallet:
|
||||||
|
definition.authorWalletPublicKey ??
|
||||||
|
(await ProgramStateClass.getProgramTokenAddress(
|
||||||
|
queueAccount.program
|
||||||
|
)),
|
||||||
|
batchSize: definition.oracleRequestBatchSize ?? 1,
|
||||||
|
expiration: definition.expiration
|
||||||
|
? new anchor.BN(definition.expiration)
|
||||||
|
: undefined,
|
||||||
|
keypair: definition.existingKeypair ?? undefined,
|
||||||
|
minRequiredOracleResults: definition.minRequiredOracleResults ?? 1,
|
||||||
|
minRequiredJobResults: definition.minRequiredJobResults ?? 1,
|
||||||
|
minUpdateDelaySeconds: definition.minUpdateDelaySeconds ?? 30,
|
||||||
|
name: definition.name ? Buffer.from(definition.name) : undefined,
|
||||||
|
metadata: definition.metadata
|
||||||
|
? Buffer.from(definition.metadata)
|
||||||
|
: undefined,
|
||||||
|
queueAccount,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (aggregatorAccount.keypair) {
|
||||||
|
context.fs.saveKeypair(aggregatorAccount.keypair);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (definition.historyBuffer) {
|
||||||
|
const size = Math.floor(definition.historyBuffer);
|
||||||
|
await aggregatorAccount.setHistoryBuffer({
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
context.logger.debug(`created history buffer of size ${size}`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
context.logger.info(
|
||||||
|
`created aggregator ${definition.name} ${aggregatorAccount.publicKey}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await AggregatorClass.buildJobs(
|
||||||
|
context,
|
||||||
|
aggregatorAccount,
|
||||||
|
definition.jobs
|
||||||
|
);
|
||||||
|
return AggregatorClass.init(context, aggregatorAccount, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromCopyAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
definition: copyAccount
|
||||||
|
) {
|
||||||
|
const wallet = programWallet(queueAccount.program);
|
||||||
|
const sourceAggregator = new AggregatorAccount({
|
||||||
|
program: queueAccount.program,
|
||||||
|
publicKey: definition.sourcePublicKey,
|
||||||
|
});
|
||||||
|
const source = await AggregatorClass.fromAccount(context, sourceAggregator);
|
||||||
|
|
||||||
|
const variance = new Big(source.varianceThreshold.mantissa.toString()).div(
|
||||||
|
new Big(10).pow(source.varianceThreshold.scale)
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetDefinition: fromAggregatorJSON = {
|
||||||
|
...source.toJSON(),
|
||||||
|
authorityPublicKey:
|
||||||
|
definition.authorityKeypair?.publicKey || wallet.publicKey,
|
||||||
|
crank: undefined,
|
||||||
|
expiration: source.expiration.toString(),
|
||||||
|
forceReportPeriod: source.forceReportPeriod.toString(),
|
||||||
|
queuePublicKey: queueAccount.publicKey,
|
||||||
|
varianceThreshold: variance.toNumber(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return AggregatorClass.fromJSON(context, queueAccount, targetDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addJob(
|
||||||
|
jobDefinition: JobDefinition,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<number> {
|
||||||
|
const job = await JobClass.build(
|
||||||
|
context,
|
||||||
|
this.account.program,
|
||||||
|
jobDefinition
|
||||||
|
);
|
||||||
|
await this.account.addJob(
|
||||||
|
job.account,
|
||||||
|
getProgramPayer(this.account.program)
|
||||||
|
);
|
||||||
|
this.jobs.push(job);
|
||||||
|
const newJobIndex = this.jobs.findIndex((existingJob) =>
|
||||||
|
existingJob.publicKey.equals(job.publicKey)
|
||||||
|
);
|
||||||
|
if (newJobIndex === -1) {
|
||||||
|
throw new Error(`failed to find new job in aggregator`);
|
||||||
|
}
|
||||||
|
return newJobIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeJob(jobKey: PublicKey, authority?: Keypair): Promise<JobClass> {
|
||||||
|
const removeIndex = this.jobs.findIndex((job) =>
|
||||||
|
job.publicKey.equals(jobKey)
|
||||||
|
);
|
||||||
|
if (removeIndex === -1) {
|
||||||
|
throw new Error(`failed to remove job with publicKey ${jobKey}`);
|
||||||
|
}
|
||||||
|
const removedJob = this.jobs[removeIndex];
|
||||||
|
await this.account.removeJob(removedJob.account, authority);
|
||||||
|
this.jobs = this.jobs.filter((job, index) => index !== removeIndex);
|
||||||
|
return removedJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
async extendLease(
|
||||||
|
funderTokenAccount: PublicKey,
|
||||||
|
amount: anchor.BN
|
||||||
|
): Promise<string> {
|
||||||
|
return this.leaseAccount.account.extend({
|
||||||
|
funder: funderTokenAccount,
|
||||||
|
funderAuthority: getProgramPayer(this.account.program),
|
||||||
|
loadAmount: amount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addHistoryBuffer(size: number, authority?: Keypair): Promise<string> {
|
||||||
|
return this.account.setHistoryBuffer({
|
||||||
|
size,
|
||||||
|
authority: authority || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async buildJobs(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregatorAccount: AggregatorAccount,
|
||||||
|
jobs: JobDefinition[]
|
||||||
|
) {
|
||||||
|
const newJobs: JobClass[] = [];
|
||||||
|
for await (const jobDefinition of jobs) {
|
||||||
|
try {
|
||||||
|
const newJob = await JobClass.build(
|
||||||
|
context,
|
||||||
|
aggregatorAccount.program,
|
||||||
|
jobDefinition
|
||||||
|
);
|
||||||
|
newJobs.push(newJob);
|
||||||
|
await aggregatorAccount.addJob(
|
||||||
|
newJob.account,
|
||||||
|
getProgramPayer(aggregatorAccount.program)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
context.logger.log(
|
||||||
|
`failed to add job to aggregator ${error.message}\r\n${jobDefinition} `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async grantPermission(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAuthority = programWallet(this.account.program)
|
||||||
|
): Promise<string> {
|
||||||
|
if (
|
||||||
|
this.permissionAccount.permission === "NONE" &&
|
||||||
|
this.authorityPublicKey.equals(
|
||||||
|
programWallet(this.account.program).publicKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.permissionAccount = await PermissionClass.grantPermission(
|
||||||
|
context,
|
||||||
|
this.account,
|
||||||
|
queueAuthority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.permissionAccount.permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateReady(aggregator: AggregatorAccountData) {
|
||||||
|
const timestamp: anchor.BN = new anchor.BN(Math.round(Date.now() / 1000));
|
||||||
|
const minUpdateDelay: number = aggregator.minUpdateDelaySeconds;
|
||||||
|
const currentTimestamp = aggregator.currentRound.roundOpenTimestamp;
|
||||||
|
const diff = timestamp.sub(currentTimestamp).abs().toNumber();
|
||||||
|
|
||||||
|
if (diff < minUpdateDelay) {
|
||||||
|
throw new AggregatorIllegalRoundOpenCall(
|
||||||
|
`${diff} / ${minUpdateDelay} sec`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
payoutAddress?: PublicKey,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<string> {
|
||||||
|
AggregatorClass.updateReady(await this.account.loadData());
|
||||||
|
|
||||||
|
const oracleQueueAccount = new OracleQueueAccount({
|
||||||
|
program: this.account.program,
|
||||||
|
publicKey: this.queuePublicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payoutWallet =
|
||||||
|
payoutAddress ??
|
||||||
|
(await ProgramStateClass.getProgramTokenAddress(
|
||||||
|
this.account.program,
|
||||||
|
context
|
||||||
|
));
|
||||||
|
|
||||||
|
return this.account.openRound({
|
||||||
|
oracleQueueAccount,
|
||||||
|
payoutWallet,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getJobs(
|
||||||
|
aggregatorAccount: AggregatorAccount,
|
||||||
|
aggregatorData?: Promise<AggregatorAccountData>,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<JobClass[]> {
|
||||||
|
const data: AggregatorAccountData = aggregatorData
|
||||||
|
? await aggregatorData
|
||||||
|
: await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
const jobs: JobClass[] = [];
|
||||||
|
for await (const jobKey of data.jobPubkeysData) {
|
||||||
|
if (!jobKey.equals(DEFAULT_PUBKEY)) {
|
||||||
|
const job = await JobClass.build(context, aggregatorAccount.program, {
|
||||||
|
publicKey: jobKey,
|
||||||
|
});
|
||||||
|
jobs.push(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads onchain jobs, lease, permission, and account data
|
||||||
|
async loadData() {
|
||||||
|
const dataPromise: Promise<AggregatorAccountData> = this.account.loadData();
|
||||||
|
this.jobs = await AggregatorClass.getJobs(this.account, dataPromise);
|
||||||
|
const data = await dataPromise;
|
||||||
|
|
||||||
|
this.result = "";
|
||||||
|
try {
|
||||||
|
this.result = new SwitchboardDecimal(
|
||||||
|
data.latestConfirmedRound.result.mantissa ?? new anchor.BN(0),
|
||||||
|
data.latestConfirmedRound.result.scale ?? 0
|
||||||
|
)
|
||||||
|
.toBig()
|
||||||
|
.toString();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
this.resultTimestamp = anchorBNtoDateTimeString(
|
||||||
|
data.latestConfirmedRound.roundOpenTimestamp
|
||||||
|
);
|
||||||
|
this.publicKey = this.account.publicKey;
|
||||||
|
this.authorWalletPublicKey = data.authorWallet;
|
||||||
|
this.authorityPublicKey = data.authority;
|
||||||
|
this.crankPublicKey = data.crankPubkey;
|
||||||
|
this.historyBufferPublicKey = data.historyBuffer;
|
||||||
|
this.oracleRequestBatchSize = data.oracleRequestBatchSize;
|
||||||
|
this.expiration = data.expiration;
|
||||||
|
this.forceReportPeriod = data.forceReportPeriod;
|
||||||
|
this.isLocked = data.isLocked;
|
||||||
|
this.metadata = buffer2string(data.metadata as any);
|
||||||
|
this.minRequiredJobResults = data.minJobResults;
|
||||||
|
this.minRequiredOracleResults = data.minOracleResults;
|
||||||
|
this.minUpdateDelaySeconds = data.minUpdateDelaySeconds;
|
||||||
|
this.name = buffer2string(data.name as any);
|
||||||
|
this.queuePublicKey = data.queuePubkey;
|
||||||
|
this.startAfter = data.startAfter.toNumber();
|
||||||
|
this.varianceThreshold = data.varianceThreshold as SwitchboardDecimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): IAggregatorClass {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
metadata: this.metadata,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
authorityPublicKey: this.authorityPublicKey,
|
||||||
|
crankPublicKey: this.crankPublicKey,
|
||||||
|
authorWalletPublicKey: this.authorWalletPublicKey,
|
||||||
|
oracleRequestBatchSize: this.oracleRequestBatchSize,
|
||||||
|
expiration: this.expiration,
|
||||||
|
forceReportPeriod: this.forceReportPeriod,
|
||||||
|
isLocked: this.isLocked,
|
||||||
|
minRequiredJobResults: this.minRequiredJobResults,
|
||||||
|
minRequiredOracleResults: this.minRequiredOracleResults,
|
||||||
|
minUpdateDelaySeconds: this.minUpdateDelaySeconds,
|
||||||
|
queuePublicKey: this.queuePublicKey,
|
||||||
|
startAfter: this.startAfter,
|
||||||
|
varianceThreshold: this.varianceThreshold,
|
||||||
|
leaseAccount: this.leaseAccount.toJSON(),
|
||||||
|
permissionAccount: this.permissionAccount.toJSON(),
|
||||||
|
jobs: this.jobs ? this.jobs.map((job) => job.toJSON()) : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false, SPACING = 24): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Aggregator", this.account.publicKey.toString(), SPACING) +
|
||||||
|
"\r\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"latestResult",
|
||||||
|
`${this.result} (${this.resultTimestamp})`,
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
|
||||||
|
outputString += chalkString("name", this.name, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("metadata", this.metadata, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("authority", this.authorityPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("queuePubkey", this.queuePublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("crankPubkey", this.crankPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"historyBufferPublicKey",
|
||||||
|
this.historyBufferPublicKey,
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("authorWallet", this.authorWalletPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("jobPubkeysSize", this.jobs.length, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("minJobResults", this.minRequiredJobResults, SPACING) +
|
||||||
|
"\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"oracleRequestBatchSize",
|
||||||
|
this.oracleRequestBatchSize,
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("minOracleResults", this.minRequiredOracleResults, SPACING) +
|
||||||
|
"\r\n";
|
||||||
|
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"varianceThreshold",
|
||||||
|
SwitchboardDecimal.from(this.varianceThreshold).toBig().toString(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"minUpdateDelaySeconds",
|
||||||
|
this.minUpdateDelaySeconds,
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"forceReportPeriod",
|
||||||
|
this.forceReportPeriod.toNumber(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString += chalkString("isLocked", this.isLocked, SPACING) + "\r\n";
|
||||||
|
|
||||||
|
if (all) {
|
||||||
|
if (this.permissionAccount) {
|
||||||
|
outputString += `\r\n${this.permissionAccount.prettyPrint(
|
||||||
|
true,
|
||||||
|
SPACING
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (this.leaseAccount) {
|
||||||
|
outputString += `\r\n${this.leaseAccount.prettyPrint(true, SPACING)}`;
|
||||||
|
}
|
||||||
|
for (const job of this.jobs) {
|
||||||
|
outputString += `\r\n${job.prettyPrint(true)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./aggregator";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,121 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import { SwitchboardDecimal } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import {
|
||||||
|
copyAccount,
|
||||||
|
fromPublicKey,
|
||||||
|
fromSwitchboardAccount,
|
||||||
|
IJobClass,
|
||||||
|
ILeaseClass,
|
||||||
|
IPermissionClass,
|
||||||
|
JobDefinition,
|
||||||
|
jsonPath,
|
||||||
|
PermissionDefinition,
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
export interface AggregatorHistoryRow {
|
||||||
|
timestamp: anchor.BN;
|
||||||
|
value: SwitchboardDecimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AggregatorRound {
|
||||||
|
numSuccess: number;
|
||||||
|
numError: number;
|
||||||
|
isClosed: boolean;
|
||||||
|
roundOpenSlot: anchor.BN;
|
||||||
|
roundOpenTimestamp: anchor.BN;
|
||||||
|
result: SwitchboardDecimal;
|
||||||
|
stdDeviation: SwitchboardDecimal;
|
||||||
|
minResponse: SwitchboardDecimal;
|
||||||
|
maxResponse: SwitchboardDecimal;
|
||||||
|
oraclePubkeysData: PublicKey[];
|
||||||
|
mediansData: SwitchboardDecimal[];
|
||||||
|
currentPayout: anchor.BN[];
|
||||||
|
mediansFulfilled: boolean[];
|
||||||
|
errorsFulfilled: boolean[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AggregatorAccountData {
|
||||||
|
name: Buffer;
|
||||||
|
metadata: Buffer;
|
||||||
|
authorWallet: PublicKey;
|
||||||
|
queuePubkey: PublicKey;
|
||||||
|
crankPubkey: PublicKey;
|
||||||
|
oracleRequestBatchSize: number;
|
||||||
|
minOracleResults: number;
|
||||||
|
minJobResults: number;
|
||||||
|
minUpdateDelaySeconds: number;
|
||||||
|
startAfter: anchor.BN;
|
||||||
|
varianceThreshold: SwitchboardDecimal;
|
||||||
|
forceReportPeriod: anchor.BN;
|
||||||
|
expiration: anchor.BN;
|
||||||
|
consecutiveFailureCount: anchor.BN;
|
||||||
|
nextAllowedUpdateTime: anchor.BN;
|
||||||
|
isLocked: boolean;
|
||||||
|
schedule: Buffer;
|
||||||
|
latestConfirmedRound: AggregatorRound;
|
||||||
|
currentRound: AggregatorRound;
|
||||||
|
jobPubkeysData: PublicKey[]; // is there a way to define sizeof 16?
|
||||||
|
jobHashes: Buffer; // Hash[16]
|
||||||
|
jobPubkeysSize: number;
|
||||||
|
jobsChecksum: Buffer;
|
||||||
|
authority: PublicKey;
|
||||||
|
historyBuffer: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON interface to construct a new Aggregator Account */
|
||||||
|
export interface fromAggregatorJSON {
|
||||||
|
authorWalletPublicKey?: PublicKey;
|
||||||
|
authorityPublicKey?: PublicKey;
|
||||||
|
oracleRequestBatchSize?: number; // REQ, will default to some value
|
||||||
|
crank?: string | number | boolean | undefined;
|
||||||
|
expiration?: string | number; // BN
|
||||||
|
forceReportPeriod?: string | number; // BN
|
||||||
|
existingKeypair?: Keypair; // TODO: fs path to keypair
|
||||||
|
metadata?: string;
|
||||||
|
minRequiredJobResults?: number; // REQ, will default to 75% of jobs
|
||||||
|
minRequiredOracleResults?: number; // REQ, will default to 1
|
||||||
|
minUpdateDelaySeconds?: number; // REQ, will default to 30s
|
||||||
|
name?: string;
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
startAfter?: number;
|
||||||
|
varianceThreshold?: number;
|
||||||
|
historyBuffer?: number;
|
||||||
|
// accounts
|
||||||
|
jobs: JobDefinition[];
|
||||||
|
leaseAccount?: ILeaseClass;
|
||||||
|
permissionAccount?: PermissionDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Aggregator Account */
|
||||||
|
export interface IAggregatorClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
authorWalletPublicKey: PublicKey;
|
||||||
|
historyBufferPublicKey?: PublicKey;
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
oracleRequestBatchSize: number; // REQ, will default to some value
|
||||||
|
crankPublicKey?: PublicKey | string | number | boolean;
|
||||||
|
expiration: anchor.BN;
|
||||||
|
forceReportPeriod: anchor.BN;
|
||||||
|
isLocked?: boolean;
|
||||||
|
metadata: string;
|
||||||
|
minRequiredJobResults: number; // REQ, will default to 75% of jobs
|
||||||
|
minRequiredOracleResults: number; // REQ, will default to 1
|
||||||
|
minUpdateDelaySeconds: number; // REQ, will default to 30s
|
||||||
|
name: string;
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
startAfter: number;
|
||||||
|
varianceThreshold: SwitchboardDecimal;
|
||||||
|
|
||||||
|
jobs: IJobClass[];
|
||||||
|
leaseAccount?: ILeaseClass;
|
||||||
|
permissionAccount?: IPermissionClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type representing the different ways to build an Aggregator Account */
|
||||||
|
export type AggregatorDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| fromAggregatorJSON
|
||||||
|
| copyAccount
|
||||||
|
| jsonPath;
|
|
@ -0,0 +1,215 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
CrankAccount,
|
||||||
|
CrankRow,
|
||||||
|
OracleQueueAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { buffer2string, chalkString, pubKeyConverter } from "../";
|
||||||
|
import { CommandContext } from "../../types/context";
|
||||||
|
import { DEFAULT_CONTEXT } from "../../types/context/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { AggregatorClass } from "../aggregator";
|
||||||
|
import { ProgramStateClass } from "../state";
|
||||||
|
import {
|
||||||
|
CrankAccountData,
|
||||||
|
CrankDefinition,
|
||||||
|
fromCrankJSON,
|
||||||
|
ICrankClass,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class CrankClass implements ICrankClass {
|
||||||
|
account: CrankAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
|
||||||
|
maxRows: number;
|
||||||
|
|
||||||
|
metadata: string;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
aggregatorKeys: PublicKey[];
|
||||||
|
|
||||||
|
dataBuffer: PublicKey;
|
||||||
|
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private static async init(
|
||||||
|
context: CommandContext,
|
||||||
|
account: CrankAccount
|
||||||
|
): Promise<CrankClass> {
|
||||||
|
const crank = new CrankClass();
|
||||||
|
crank.logger = context.logger;
|
||||||
|
|
||||||
|
crank.account = account;
|
||||||
|
crank.publicKey = crank.account.publicKey;
|
||||||
|
|
||||||
|
await crank.loadData();
|
||||||
|
|
||||||
|
return crank;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async build(
|
||||||
|
context,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: CrankDefinition,
|
||||||
|
queueAccount?: OracleQueueAccount
|
||||||
|
) {
|
||||||
|
if ("account" in definition) {
|
||||||
|
if (definition.account instanceof CrankAccount) {
|
||||||
|
return CrankClass.fromAccount(context, definition.account);
|
||||||
|
}
|
||||||
|
throw new TypeError(`account type should be CrankAccount`);
|
||||||
|
} else if ("publicKey" in definition) {
|
||||||
|
return CrankClass.fromPublicKey(context, program, definition.publicKey);
|
||||||
|
} else if (queueAccount) {
|
||||||
|
return CrankClass.fromJSON(context, definition, queueAccount);
|
||||||
|
}
|
||||||
|
throw new Error("failed to build crank class");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
account: CrankAccount
|
||||||
|
) {
|
||||||
|
return CrankClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
) {
|
||||||
|
return CrankClass.init(
|
||||||
|
context,
|
||||||
|
new CrankAccount({
|
||||||
|
program,
|
||||||
|
publicKey,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromJSON(
|
||||||
|
context: CommandContext,
|
||||||
|
definition: fromCrankJSON,
|
||||||
|
queueAccount: OracleQueueAccount
|
||||||
|
) {
|
||||||
|
const { name, metadata, maxRows } = definition;
|
||||||
|
|
||||||
|
const account = await CrankAccount.create(queueAccount.program, {
|
||||||
|
queueAccount,
|
||||||
|
name: name ? Buffer.from(name) : undefined,
|
||||||
|
metadata: metadata ? Buffer.from(metadata) : undefined,
|
||||||
|
maxRows,
|
||||||
|
});
|
||||||
|
|
||||||
|
context.logger.info(`created crank account ${name} ${account.publicKey}`);
|
||||||
|
|
||||||
|
return CrankClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromDefault(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
name = ""
|
||||||
|
) {
|
||||||
|
return CrankClass.fromJSON(context, { name }, queueAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async turn(
|
||||||
|
crankAccount: CrankAccount,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<string> {
|
||||||
|
const { queuePubkey } = await crankAccount.loadData();
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: crankAccount.program,
|
||||||
|
publicKey: queuePubkey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { authority } = await queueAccount.loadData();
|
||||||
|
const authorityTokenWallet = await ProgramStateClass.getProgramTokenAddress(
|
||||||
|
crankAccount.program
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = await ProgramStateClass.build(crankAccount.program, context);
|
||||||
|
|
||||||
|
const popTxn = await crankAccount.pop({
|
||||||
|
payoutWallet: authorityTokenWallet,
|
||||||
|
queuePubkey,
|
||||||
|
queueAuthority: authority,
|
||||||
|
crank: 0,
|
||||||
|
queue: 0,
|
||||||
|
tokenMint: state.tokenMintPublicKey,
|
||||||
|
});
|
||||||
|
return popTxn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async push(aggregator: AggregatorClass): Promise<string> {
|
||||||
|
return this.account.push({ aggregatorAccount: aggregator.account });
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: CrankAccountData | any = await this.account.loadData();
|
||||||
|
this.aggregatorKeys = data.pqData
|
||||||
|
.slice(0, data.pqSize)
|
||||||
|
.map((item: CrankRow) => item.pubkey);
|
||||||
|
|
||||||
|
this.name = buffer2string(data.name as any);
|
||||||
|
this.metadata = buffer2string(data.metadata as any);
|
||||||
|
this.publicKey = this.account.publicKey;
|
||||||
|
this.queuePublicKey = data.queuePubkey;
|
||||||
|
this.maxRows = data.maxRows;
|
||||||
|
this.dataBuffer = data.dataBuffer;
|
||||||
|
this.size = data.pqSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): ICrankClass {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
metadata: this.metadata,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
queuePublicKey: this.queuePublicKey,
|
||||||
|
maxRows: this.maxRows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false, SPACING = 30): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Crank", this.publicKey.toString(), SPACING) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString += chalkString("name", this.name, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("metadata", this.metadata, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("dataBuffer", this.dataBuffer, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("queuePubkey", this.queuePublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("rows", `${this.size} / ${this.maxRows}`, SPACING) + "\r\n";
|
||||||
|
|
||||||
|
if (all) {
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"maxRows",
|
||||||
|
JSON.stringify(this.aggregatorKeys, pubKeyConverter, 2),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./crank";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { CrankRow } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import { fromPublicKey, fromSwitchboardAccount } from "..";
|
||||||
|
|
||||||
|
export interface CrankAccountData {
|
||||||
|
name: Buffer;
|
||||||
|
metadata: Buffer;
|
||||||
|
queuePubkey: PublicKey;
|
||||||
|
pqSize: number;
|
||||||
|
maxRows: number;
|
||||||
|
jitterModifier: number; // u8
|
||||||
|
pqData: CrankRow[];
|
||||||
|
dataBuffer: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON interface to construct a new Crank Account */
|
||||||
|
export interface fromCrankJSON {
|
||||||
|
name?: string;
|
||||||
|
metadata?: string;
|
||||||
|
maxRows?: number;
|
||||||
|
queuePublicKey?: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Crank Account */
|
||||||
|
export interface ICrankClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
maxRows: number;
|
||||||
|
metadata: string;
|
||||||
|
name: string;
|
||||||
|
queuePublicKey?: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type representing the different ways to build a Crank Account */
|
||||||
|
export type CrankDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| fromCrankJSON;
|
||||||
|
|
||||||
|
/** Type representing the different ways to build a set of Crank Accounts */
|
||||||
|
export type CrankDefinitions = CrankDefinition[] | number;
|
|
@ -0,0 +1,10 @@
|
||||||
|
export * from "./aggregator";
|
||||||
|
export * from "./crank";
|
||||||
|
export * from "./job";
|
||||||
|
export * from "./lease";
|
||||||
|
export * from "./oracle";
|
||||||
|
export * from "./permission";
|
||||||
|
export * from "./queue";
|
||||||
|
export * from "./state";
|
||||||
|
export * from "./types";
|
||||||
|
export * from "./utils";
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import { TemplateSource, TEMPLATE_SOURCES } from ".";
|
||||||
|
import { Ascendex } from "./jobTemplates/ascendex";
|
||||||
|
import { BinanceCom } from "./jobTemplates/binanceCom";
|
||||||
|
import { BinanceUs } from "./jobTemplates/binanceUs";
|
||||||
|
import { Bitfinex } from "./jobTemplates/bitfinex";
|
||||||
|
import { Bitstamp } from "./jobTemplates/bitstamp";
|
||||||
|
import { Bittrex } from "./jobTemplates/bittrex";
|
||||||
|
import { Bonfida } from "./jobTemplates/bonfida";
|
||||||
|
import { Coinbase } from "./jobTemplates/coinbase";
|
||||||
|
import { FtxCom } from "./jobTemplates/ftxCom";
|
||||||
|
import { FtxUs } from "./jobTemplates/ftxUs";
|
||||||
|
import { Gate } from "./jobTemplates/gate";
|
||||||
|
import { Huobi } from "./jobTemplates/huobi";
|
||||||
|
import { Kraken } from "./jobTemplates/kraken";
|
||||||
|
import { Kucoin } from "./jobTemplates/kucoin";
|
||||||
|
import { Mexc } from "./jobTemplates/mexc";
|
||||||
|
import { Okex } from "./jobTemplates/okex";
|
||||||
|
import { Orca } from "./jobTemplates/orca";
|
||||||
|
import { Raydium } from "./jobTemplates/raydium";
|
||||||
|
import { SMB } from "./jobTemplates/smb";
|
||||||
|
|
||||||
|
export const buildJobTasks = async (
|
||||||
|
source: TemplateSource,
|
||||||
|
id?: string
|
||||||
|
): Promise<OracleJob.Task[]> => {
|
||||||
|
switch (source.toLowerCase()) {
|
||||||
|
case "ascendex":
|
||||||
|
return new Ascendex(id).tasks();
|
||||||
|
case "binancecom":
|
||||||
|
return new BinanceCom(id).tasks();
|
||||||
|
case "binanceus":
|
||||||
|
return new BinanceUs(id).tasks();
|
||||||
|
case "bitfinex":
|
||||||
|
return new Bitfinex(id).tasks();
|
||||||
|
case "bitstamp":
|
||||||
|
return new Bitstamp(id).tasks();
|
||||||
|
case "bittrex":
|
||||||
|
return new Bittrex(id).tasks();
|
||||||
|
case "bonfida":
|
||||||
|
return new Bonfida(id).tasks();
|
||||||
|
case "coinbase":
|
||||||
|
return new Coinbase(id).tasks();
|
||||||
|
case "ftxcom":
|
||||||
|
return new FtxCom(id).tasks();
|
||||||
|
case "ftxus":
|
||||||
|
return new FtxUs(id).tasks();
|
||||||
|
case "gate":
|
||||||
|
return new Gate(id).tasks();
|
||||||
|
case "huobi":
|
||||||
|
return new Huobi(id).tasks();
|
||||||
|
case "kraken":
|
||||||
|
return new Kraken(id).tasks();
|
||||||
|
case "kucoin":
|
||||||
|
return new Kucoin(id).tasks();
|
||||||
|
case "mexc":
|
||||||
|
return new Mexc(id).tasks();
|
||||||
|
case "okex":
|
||||||
|
return new Okex(id).tasks();
|
||||||
|
case "orca":
|
||||||
|
return new Orca(id).tasks();
|
||||||
|
case "raydium":
|
||||||
|
return new Raydium(id).tasks();
|
||||||
|
case "smb":
|
||||||
|
return new SMB(id).tasks();
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`No job template found for ${source}. Available options:\r\n${TEMPLATE_SOURCES}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./buildTemplate";
|
||||||
|
export * from "./job";
|
||||||
|
export * from "./types";
|
||||||
|
export * from "./utils";
|
|
@ -0,0 +1,216 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
JobAccount,
|
||||||
|
OracleJob,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { getUrlFromTask } from ".";
|
||||||
|
import { buffer2string, chalkString, copyAccount, pubKeyConverter } from "../";
|
||||||
|
import { CommandContext } from "../../types/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { ProgramStateClass } from "../state";
|
||||||
|
import { buildJobTasks } from "./buildTemplate";
|
||||||
|
import {
|
||||||
|
fromJobJSON,
|
||||||
|
fromJobTemplate,
|
||||||
|
IJobClass,
|
||||||
|
JobAccountData,
|
||||||
|
JobDefinition,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class JobClass implements IJobClass {
|
||||||
|
account: JobAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
authorityWalletPublicKey: PublicKey;
|
||||||
|
|
||||||
|
expiration: anchor.BN;
|
||||||
|
|
||||||
|
metadata: string;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
tasks: OracleJob.ITask[];
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private static async init(
|
||||||
|
context: CommandContext,
|
||||||
|
account: JobAccount
|
||||||
|
): Promise<JobClass> {
|
||||||
|
const job = new JobClass();
|
||||||
|
job.account = account;
|
||||||
|
job.publicKey = job.account.publicKey;
|
||||||
|
job.logger = context.logger;
|
||||||
|
|
||||||
|
await job.loadData();
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async build(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: JobDefinition
|
||||||
|
): Promise<JobClass> {
|
||||||
|
if ("account" in definition) {
|
||||||
|
if (definition.account instanceof JobAccount) {
|
||||||
|
return JobClass.fromAccount(context, definition.account);
|
||||||
|
}
|
||||||
|
throw new TypeError(`account type should be CrankAccount`);
|
||||||
|
} else if ("publicKey" in definition) {
|
||||||
|
return JobClass.fromPublicKey(context, program, definition.publicKey);
|
||||||
|
} else if ("template" in definition) {
|
||||||
|
return JobClass.fromTemplate(context, program, definition);
|
||||||
|
} else if ("tasks" in definition) {
|
||||||
|
return JobClass.fromJSON(context, program, definition);
|
||||||
|
} else if ("sourcePublicKey" in definition) {
|
||||||
|
return JobClass.fromCopyAccount(context, program, definition);
|
||||||
|
} else {
|
||||||
|
throw new Error(`failed to build job from definition ${definition}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromAccount(context: CommandContext, account: JobAccount) {
|
||||||
|
return JobClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
) {
|
||||||
|
return JobClass.init(
|
||||||
|
context,
|
||||||
|
new JobAccount({
|
||||||
|
program,
|
||||||
|
publicKey,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromTemplate(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: fromJobTemplate
|
||||||
|
) {
|
||||||
|
const tasks = await buildJobTasks(definition.template, definition.id);
|
||||||
|
const job = OracleJob.create({ tasks });
|
||||||
|
const data = Buffer.from(OracleJob.encodeDelimited(job).finish());
|
||||||
|
const jobUrl = getUrlFromTask(job);
|
||||||
|
const wallet = programWallet(program);
|
||||||
|
const account = await JobAccount.create(program, {
|
||||||
|
data,
|
||||||
|
name: Buffer.from(`${jobUrl} ${definition.id}`),
|
||||||
|
authority: wallet.publicKey,
|
||||||
|
});
|
||||||
|
return JobClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromJSON(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: fromJobJSON
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
tasks,
|
||||||
|
expiration,
|
||||||
|
authorityWalletPublicKey,
|
||||||
|
existingKeypair,
|
||||||
|
} = definition;
|
||||||
|
const data = Buffer.from(
|
||||||
|
OracleJob.encodeDelimited(
|
||||||
|
OracleJob.create({
|
||||||
|
tasks,
|
||||||
|
})
|
||||||
|
).finish()
|
||||||
|
);
|
||||||
|
|
||||||
|
const keypair = existingKeypair ?? anchor.web3.Keypair.generate();
|
||||||
|
const account = await JobAccount.create(program, {
|
||||||
|
data: data,
|
||||||
|
name: name ? Buffer.from(name) : Buffer.from(""),
|
||||||
|
expiration: expiration ? new anchor.BN(expiration) : undefined,
|
||||||
|
authority:
|
||||||
|
authorityWalletPublicKey ??
|
||||||
|
(await ProgramStateClass.getProgramTokenAddress(program)),
|
||||||
|
keypair,
|
||||||
|
});
|
||||||
|
|
||||||
|
context.fs.saveKeypair(keypair);
|
||||||
|
|
||||||
|
context.logger.info(`created job account ${name} ${account.publicKey}`);
|
||||||
|
|
||||||
|
return JobClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromCopyAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: copyAccount
|
||||||
|
) {
|
||||||
|
const sourceJob = new JobAccount({
|
||||||
|
program,
|
||||||
|
publicKey: definition.sourcePublicKey,
|
||||||
|
});
|
||||||
|
const jobData: JobAccountData = await sourceJob.loadData();
|
||||||
|
const account = await JobAccount.create(program, {
|
||||||
|
data: jobData.data,
|
||||||
|
name: Buffer.from(jobData.name),
|
||||||
|
expiration: jobData.expiration
|
||||||
|
? new anchor.BN(jobData.expiration)
|
||||||
|
: undefined,
|
||||||
|
authority: jobData.authority ?? undefined,
|
||||||
|
});
|
||||||
|
return JobClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: JobAccountData = await this.account.loadData();
|
||||||
|
|
||||||
|
this.authorityWalletPublicKey = data.authority;
|
||||||
|
this.expiration = data.expiration;
|
||||||
|
this.metadata = buffer2string(data.metadata as any);
|
||||||
|
this.name = buffer2string(data.name as any);
|
||||||
|
this.tasks = OracleJob.decodeDelimited(data.data).tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): IJobClass {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
metadata: this.metadata,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
authorityWalletPublicKey: this.authorityWalletPublicKey,
|
||||||
|
expiration: this.expiration,
|
||||||
|
tasks: this.tasks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Job", this.publicKey) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString += chalkString("name", this.name) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("authority", this.authorityWalletPublicKey) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("expiration", this.expiration.toString()) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("tasks", JSON.stringify(this.tasks, undefined, 2)) + "\r\n";
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Ascendex extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return `https://ascendex.com/api/pro/v1/ticker?symbol=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: ``,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$.data.ask[0]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$.data.bid[0]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$.data.close`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class BinanceCom extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return `https://www.binance.com/api/v3/ticker/price?symbol=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.price" }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class BinanceUs extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://www.binance.us/api/v3/ticker/price?symbol=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.price" }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Bitfinex extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
const cleanedupId =
|
||||||
|
this.id.charAt(0).toLowerCase() + this.id.toUpperCase().slice(1);
|
||||||
|
return `https://api-pub.bitfinex.com/v2/tickers?symbols=${cleanedupId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$[0][1]",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$[0][3]",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$[0][7]",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Bitstamp extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return `https://www.bitstamp.net/api/v2/ticker/${this.id.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.ask" }),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.bid" }),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.last" }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Bittrex extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return `https://api.bittrex.com/v3/markets/${this.id}/ticker`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.askRate",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.bidRate",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.lastTradeRate",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Bonfida extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return `https://serum-api.bonfida.com/orderbooks/${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: ``,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data.bids[0].price",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data.asks[0].price",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Coinbase extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `wss://ws-feed.pro.coinbase.com`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
websocketTask: OracleJob.WebsocketTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
subscription: JSON.stringify({
|
||||||
|
type: "subscribe",
|
||||||
|
product_ids: [this.id],
|
||||||
|
channels: [
|
||||||
|
"ticker",
|
||||||
|
{
|
||||||
|
name: "ticker",
|
||||||
|
product_ids: [this.id],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
maxDataAgeSeconds: 15,
|
||||||
|
filter: `$[?(@.type == 'ticker' && @.product_id == '${this.id}')]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.price" }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class FtxCom extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `wss://ftx.com/ws/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
websocketTask: OracleJob.WebsocketTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
subscription: JSON.stringify({
|
||||||
|
op: "subscribe",
|
||||||
|
channel: "ticker",
|
||||||
|
market: this.id,
|
||||||
|
}),
|
||||||
|
maxDataAgeSeconds: 15,
|
||||||
|
filter: `$[?(@.type == 'update' && @.channel == 'ticker' && @.market == '${this.id}')]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data.bid",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data.ask",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data.last",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class FtxUs extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://ftx.us/api/markets/${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.result.price",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Gate extends AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return `https://api.gateio.ws/api/v4/spot/tickers?currency_pair=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$[0].lowest_ask`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$[0].highest_bid`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$[0].last`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Huobi extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://api.huobi.pro/market/detail/merged?symbol=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.tick.bid[0]",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.tick.ask[0]",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Kraken extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://api.kraken.com/0/public/Ticker?pair=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$.result.${this.id}.a[0]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$.result.${this.id}.b[0]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$.result.${this.id}.c[0]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Kucoin extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: `$.data.price` }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Mexc extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://www.mexc.com/open/api/v2/market/ticker?symbol=${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data[0].ask",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data[0].bid",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data[0].last",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Okex extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `wss://ws.okex.com:8443/ws/v5/public`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
websocketTask: OracleJob.WebsocketTask.create({
|
||||||
|
url: "wss://ws.okex.com:8443/ws/v5/public",
|
||||||
|
subscription: JSON.stringify({
|
||||||
|
op: "subscribe",
|
||||||
|
args: [{ channel: "tickers", instId: this.id }],
|
||||||
|
}),
|
||||||
|
maxDataAgeSeconds: 15,
|
||||||
|
filter:
|
||||||
|
"$[?(" +
|
||||||
|
`@.event != 'subscribe' && ` +
|
||||||
|
`@.arg.channel == 'tickers' && ` +
|
||||||
|
`@.arg.instId == '${this.id}' && ` +
|
||||||
|
`@.data[0].instType == 'SPOT' && ` +
|
||||||
|
`@.data[0].instId == '${this.id}')]`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
medianTask: OracleJob.MedianTask.create({
|
||||||
|
tasks: [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data[0].bidPx",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data[0].askPx",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: "$.data[0].last",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Orca extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://api.orca.so/pools`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({
|
||||||
|
path: `$[?(@.name == '${this.id}[aquafarm]')].price`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
|
||||||
|
export async function buildOrcaLpTask(
|
||||||
|
key: string,
|
||||||
|
solKey: PublicKey
|
||||||
|
): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
lpExchangeRateTask: OracleJob.LpExchangeRateTask.create({
|
||||||
|
saberPoolAddress: key,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
multiplyTask: OracleJob.MultiplyTask.create({
|
||||||
|
aggregatorPubkey: solKey.toBase58(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class Raydium extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://api.raydium.io/coin/price?coins`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: OracleJob.HttpTask.create({
|
||||||
|
url: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: OracleJob.JsonParseTask.create({ path: `$.${this.id}` }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class SaberLp extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
lpTokenPriceTask: OracleJob.LpTokenPriceTask.create({
|
||||||
|
saberPoolAddress: this.url(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import AbstractJobTemplate from "./template";
|
||||||
|
|
||||||
|
export class SMB extends AbstractJobTemplate {
|
||||||
|
url(): string {
|
||||||
|
return `https://market.solanamonkey.business/.netlify/functions/fetchOffers`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async tasks(): Promise<OracleJob.Task[]> {
|
||||||
|
const tasks = [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: {
|
||||||
|
url: `https://market.solanamonkey.business/.netlify/functions/fetchOffers`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: {
|
||||||
|
path: `$.offers[?(@.price)].price`,
|
||||||
|
aggregationMethod: OracleJob.JsonParseTask.AggregationMethod.MIN,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
|
||||||
|
export function solanartFloorPrice(projectId: string): Array<OracleJob.Task> {
|
||||||
|
return [
|
||||||
|
OracleJob.Task.create({
|
||||||
|
httpTask: {
|
||||||
|
url: `https://jmccmlyu33.medianetwork.cloud/nft_for_sale?collection=${projectId}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
OracleJob.Task.create({
|
||||||
|
jsonParseTask: {
|
||||||
|
path: `$[?(@.price)].price`,
|
||||||
|
aggregationMethod: OracleJob.JsonParseTask.AggregationMethod.MIN,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
|
||||||
|
abstract class AbstractJobTemplate {
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
constructor(id: string) {
|
||||||
|
this.id = id ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract url(): string;
|
||||||
|
|
||||||
|
abstract tasks(verify?: boolean): Promise<OracleJob.Task[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AbstractJobTemplate;
|
|
@ -0,0 +1,82 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import {
|
||||||
|
copyAccount,
|
||||||
|
fromPublicKey,
|
||||||
|
fromSwitchboardAccount,
|
||||||
|
jsonPath,
|
||||||
|
} from "../types/types";
|
||||||
|
|
||||||
|
export interface JobAccountData {
|
||||||
|
name: Buffer; // Uint8Array
|
||||||
|
metadata: Buffer;
|
||||||
|
authority: PublicKey;
|
||||||
|
expiration: anchor.BN;
|
||||||
|
hash: Buffer;
|
||||||
|
data: Buffer; // ??
|
||||||
|
referenceCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEMPLATE_SOURCES = [
|
||||||
|
"ascendex",
|
||||||
|
"binanceCom",
|
||||||
|
"binanceUs",
|
||||||
|
"bitfinex",
|
||||||
|
"bitstamp",
|
||||||
|
"bittrex",
|
||||||
|
"bonfida",
|
||||||
|
"coinbase",
|
||||||
|
"ftxCom",
|
||||||
|
"ftxUs",
|
||||||
|
"gate",
|
||||||
|
"huobi",
|
||||||
|
"kraken",
|
||||||
|
"kucoin",
|
||||||
|
"mexc",
|
||||||
|
"okex",
|
||||||
|
"orca",
|
||||||
|
"raydium",
|
||||||
|
"smb",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Type representing the different predefined job templates */
|
||||||
|
export type TemplateSource = typeof TEMPLATE_SOURCES[number];
|
||||||
|
|
||||||
|
/** Create a job account from a predefined template */
|
||||||
|
export interface fromJobTemplate {
|
||||||
|
template: TemplateSource;
|
||||||
|
id?: string;
|
||||||
|
existingKeypair?: Keypair;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON interface to construct a new Job Account */
|
||||||
|
export interface fromJobJSON {
|
||||||
|
aggregator?: string | PublicKey; // add by agg name (BTC_USD)
|
||||||
|
authorityWalletPublicKey?: PublicKey; // Defaults to authority who created
|
||||||
|
existingKeypair?: Keypair;
|
||||||
|
expiration?: number;
|
||||||
|
metadata?: string;
|
||||||
|
name?: string;
|
||||||
|
tasks: OracleJob.ITask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type representing the different ways to build a Job Account */
|
||||||
|
export type JobDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| fromJobJSON
|
||||||
|
| fromJobTemplate
|
||||||
|
| copyAccount
|
||||||
|
| jsonPath;
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Job Account */
|
||||||
|
export interface IJobClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
authorityWalletPublicKey: PublicKey;
|
||||||
|
expiration: anchor.BN;
|
||||||
|
metadata: string;
|
||||||
|
name: string;
|
||||||
|
tasks: OracleJob.ITask[];
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { OracleJob } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import { URL } from "url";
|
||||||
|
|
||||||
|
export const getUrlFromTask = (job: OracleJob): string => {
|
||||||
|
const { tasks } = job;
|
||||||
|
const firstTask = tasks[0];
|
||||||
|
const jobUrl: string = firstTask.httpTask
|
||||||
|
? firstTask.httpTask.url
|
||||||
|
: firstTask.websocketTask
|
||||||
|
? firstTask.websocketTask.url
|
||||||
|
: "";
|
||||||
|
if (jobUrl === "") return jobUrl;
|
||||||
|
const parsedUrl = new URL(jobUrl);
|
||||||
|
return parsedUrl.hostname;
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./lease";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,244 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
LeaseAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import {
|
||||||
|
AggregatorAccountData,
|
||||||
|
chalkString,
|
||||||
|
LeaseAccountData,
|
||||||
|
pubKeyConverter,
|
||||||
|
} from "../";
|
||||||
|
import { CommandContext } from "../../types/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { programHasPayer } from "../../utils";
|
||||||
|
import { ProgramStateClass } from "../state";
|
||||||
|
import { ILeaseClass, LeaseDefinition } from "./types";
|
||||||
|
|
||||||
|
export class LeaseClass implements ILeaseClass {
|
||||||
|
account: LeaseAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
aggregatorPublicKey: PublicKey;
|
||||||
|
|
||||||
|
escrowPublicKey: PublicKey;
|
||||||
|
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
tokenProgramPublicKey: PublicKey;
|
||||||
|
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
|
||||||
|
withdrawAuthorityPublicKey: PublicKey;
|
||||||
|
|
||||||
|
escrowBalance: number;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private static async init(context: CommandContext, account: LeaseAccount) {
|
||||||
|
const lease = new LeaseClass();
|
||||||
|
lease.account = account;
|
||||||
|
lease.publicKey = lease.account.publicKey;
|
||||||
|
lease.logger = context.logger;
|
||||||
|
|
||||||
|
await lease.loadData();
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async build(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregatorAccount: AggregatorAccount,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
definition?: LeaseDefinition
|
||||||
|
): Promise<LeaseClass> {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
|
if (definition && "account" in definition) {
|
||||||
|
if (definition.account instanceof LeaseAccount) {
|
||||||
|
return LeaseClass.fromAccount(context, definition.account);
|
||||||
|
}
|
||||||
|
throw new TypeError("account must be an instance of PermissionAccount");
|
||||||
|
} else if (definition && "publicKey" in definition) {
|
||||||
|
return LeaseClass.fromPublicKey(
|
||||||
|
context,
|
||||||
|
aggregatorAccount.program,
|
||||||
|
definition.publicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programHasPayer(aggregatorAccount.program)) {
|
||||||
|
return LeaseClass.getOrCreateLeaseAccount(
|
||||||
|
context,
|
||||||
|
aggregatorAccount,
|
||||||
|
queueAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LeaseClass.getLeaseAccount(context, aggregatorAccount, queueAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromAccount(context: CommandContext, account: LeaseAccount) {
|
||||||
|
return LeaseClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
) {
|
||||||
|
return LeaseClass.init(
|
||||||
|
context,
|
||||||
|
new LeaseAccount({
|
||||||
|
program,
|
||||||
|
publicKey,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getLeaseAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregatorAccount: AggregatorAccount,
|
||||||
|
queueAccount?: OracleQueueAccount
|
||||||
|
): Promise<LeaseClass | undefined> {
|
||||||
|
let leaseAccount: LeaseAccount;
|
||||||
|
|
||||||
|
let queue = queueAccount;
|
||||||
|
if (!queue) {
|
||||||
|
const agg: AggregatorAccountData = await aggregatorAccount.loadData();
|
||||||
|
queue = new OracleQueueAccount({
|
||||||
|
program: aggregatorAccount.program,
|
||||||
|
publicKey: agg.queuePubkey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
[leaseAccount] = LeaseAccount.fromSeed(
|
||||||
|
aggregatorAccount.program,
|
||||||
|
queueAccount,
|
||||||
|
aggregatorAccount
|
||||||
|
);
|
||||||
|
await leaseAccount.loadData();
|
||||||
|
return await LeaseClass.init(context, leaseAccount);
|
||||||
|
} catch {
|
||||||
|
context.logger.debug(
|
||||||
|
`no lease account found for ${aggregatorAccount.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getOrCreateLeaseAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregatorAccount: AggregatorAccount,
|
||||||
|
queueAccount?: OracleQueueAccount
|
||||||
|
): Promise<LeaseClass | undefined> {
|
||||||
|
let queue = queueAccount;
|
||||||
|
if (!queue) {
|
||||||
|
const agg: AggregatorAccountData = await aggregatorAccount.loadData();
|
||||||
|
queue = new OracleQueueAccount({
|
||||||
|
program: aggregatorAccount.program,
|
||||||
|
publicKey: agg.queuePubkey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// find existing account
|
||||||
|
const lease: LeaseClass | undefined = await LeaseClass.getLeaseAccount(
|
||||||
|
context,
|
||||||
|
aggregatorAccount,
|
||||||
|
queue
|
||||||
|
);
|
||||||
|
if (lease) return lease;
|
||||||
|
|
||||||
|
// if payer, create new
|
||||||
|
if (programHasPayer(aggregatorAccount.program)) {
|
||||||
|
const programTokenWallet = await ProgramStateClass.getProgramTokenAddress(
|
||||||
|
aggregatorAccount.program
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const leaseAccount = await LeaseAccount.create(
|
||||||
|
aggregatorAccount.program,
|
||||||
|
{
|
||||||
|
aggregatorAccount,
|
||||||
|
oracleQueueAccount: queueAccount,
|
||||||
|
loadAmount: new anchor.BN(0),
|
||||||
|
funder: programTokenWallet,
|
||||||
|
funderAuthority: programWallet(aggregatorAccount.program),
|
||||||
|
withdrawAuthority: programWallet(aggregatorAccount.program)
|
||||||
|
.publicKey,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return await LeaseClass.init(context, leaseAccount);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`failed to create lease account ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBalance(): Promise<number> {
|
||||||
|
const resp =
|
||||||
|
await this.account.program.provider.connection.getTokenAccountBalance(
|
||||||
|
this.escrowPublicKey
|
||||||
|
);
|
||||||
|
return Number.parseInt(resp.value.amount, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: LeaseAccountData = await this.account.loadData();
|
||||||
|
|
||||||
|
this.publicKey = this.account.publicKey;
|
||||||
|
this.aggregatorPublicKey = data.aggregator;
|
||||||
|
this.escrowPublicKey = data.escrow;
|
||||||
|
this.isActive = data.isActive;
|
||||||
|
this.tokenProgramPublicKey = data.tokenProgram;
|
||||||
|
this.queuePublicKey = data.queue;
|
||||||
|
this.withdrawAuthorityPublicKey = data.withdrawAuthority;
|
||||||
|
|
||||||
|
this.escrowBalance = await this.getBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): ILeaseClass {
|
||||||
|
return {
|
||||||
|
publicKey: this.account.publicKey,
|
||||||
|
aggregatorPublicKey: this.aggregatorPublicKey,
|
||||||
|
queuePublicKey: this.queuePublicKey,
|
||||||
|
escrowPublicKey: this.escrowPublicKey,
|
||||||
|
isActive: this.isActive,
|
||||||
|
tokenProgramPublicKey: this.tokenProgramPublicKey,
|
||||||
|
withdrawAuthorityPublicKey: this.withdrawAuthorityPublicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false, SPACING = 24): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Lease", this.publicKey, SPACING) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString +=
|
||||||
|
chalkString("escrow", this.escrowPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("escrowBalance", this.escrowBalance, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"withdrawAuthority",
|
||||||
|
this.withdrawAuthorityPublicKey,
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString += chalkString("queue", this.queuePublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("aggregator", this.aggregatorPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("isActive", this.isActive, SPACING) + "\r\n";
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { fromPublicKey, fromSwitchboardAccount } from "..";
|
||||||
|
|
||||||
|
export interface LeaseAccountData {
|
||||||
|
escrow: PublicKey;
|
||||||
|
queue: PublicKey;
|
||||||
|
aggregator: PublicKey;
|
||||||
|
tokenProgram: PublicKey;
|
||||||
|
isActive: boolean;
|
||||||
|
crankRowCount: number;
|
||||||
|
createdAt: anchor.BN;
|
||||||
|
updateCount: anchor.BN;
|
||||||
|
withdrawAuthority: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Lease Account */
|
||||||
|
export interface ILeaseClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
aggregatorPublicKey: PublicKey;
|
||||||
|
escrowPublicKey: PublicKey;
|
||||||
|
isActive: boolean;
|
||||||
|
tokenProgramPublicKey: PublicKey;
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
withdrawAuthorityPublicKey: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LeaseDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| ILeaseClass;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./oracle";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,374 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
OracleAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import {
|
||||||
|
anchorBNtoDateTimeString,
|
||||||
|
buffer2string,
|
||||||
|
chalkString,
|
||||||
|
pubKeyConverter,
|
||||||
|
} from "../";
|
||||||
|
import { CommandContext, DEFAULT_CONTEXT } from "../../types/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { getProgramPayer } from "../../utils";
|
||||||
|
import { PermissionClass } from "../permission";
|
||||||
|
import { ProgramStateClass } from "../state";
|
||||||
|
import {
|
||||||
|
fromOracleJSON,
|
||||||
|
IOracleClass,
|
||||||
|
OracleAccountData,
|
||||||
|
OracleDefinition,
|
||||||
|
OracleMetricsData,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class OracleClass implements IOracleClass {
|
||||||
|
account: OracleAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
metadata: string;
|
||||||
|
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
|
||||||
|
tokenAccountPublicKey: PublicKey;
|
||||||
|
|
||||||
|
queuePublicKey: PublicKey;
|
||||||
|
|
||||||
|
balance: number;
|
||||||
|
|
||||||
|
lastHeartbeat: string;
|
||||||
|
|
||||||
|
numInUse: number;
|
||||||
|
|
||||||
|
metrics: OracleMetricsData;
|
||||||
|
|
||||||
|
permissionAccount?: PermissionClass;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private static async init(
|
||||||
|
context: CommandContext,
|
||||||
|
account: OracleAccount,
|
||||||
|
definition: OracleDefinition
|
||||||
|
): Promise<OracleClass> {
|
||||||
|
const oracle = new OracleClass();
|
||||||
|
oracle.logger = context.logger;
|
||||||
|
oracle.account = account;
|
||||||
|
oracle.publicKey = oracle.account.publicKey;
|
||||||
|
|
||||||
|
await oracle.loadData();
|
||||||
|
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: account.program,
|
||||||
|
publicKey: oracle.queuePublicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
oracle.permissionAccount = await PermissionClass.build(
|
||||||
|
context,
|
||||||
|
oracle.account,
|
||||||
|
queueAccount,
|
||||||
|
definition && "permissionAccount" in definition
|
||||||
|
? definition.permissionAccount
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
await oracle.loadData();
|
||||||
|
|
||||||
|
return oracle;
|
||||||
|
}
|
||||||
|
|
||||||
|
async grantPermission(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAuthority = programWallet(this.account.program)
|
||||||
|
): Promise<string> {
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: this.account.program,
|
||||||
|
publicKey: this.queuePublicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { authority } = await queueAccount.loadData();
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.permissionAccount.permission === "NONE" &&
|
||||||
|
queueAuthority.publicKey.equals(authority)
|
||||||
|
) {
|
||||||
|
const anchorWallet = programWallet(this.account.program);
|
||||||
|
this.permissionAccount = await PermissionClass.grantPermission(
|
||||||
|
context,
|
||||||
|
this.account,
|
||||||
|
anchorWallet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.permissionAccount.permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async build(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: OracleDefinition,
|
||||||
|
queueAccount?: OracleQueueAccount
|
||||||
|
): Promise<OracleClass> {
|
||||||
|
if ("account" in definition) {
|
||||||
|
if (definition.account instanceof OracleAccount) {
|
||||||
|
return OracleClass.fromAccount(context, definition.account);
|
||||||
|
}
|
||||||
|
throw new TypeError("account must be an instance of OracleAccount");
|
||||||
|
} else if ("publicKey" in definition) {
|
||||||
|
return OracleClass.fromPublicKey(context, program, definition.publicKey);
|
||||||
|
} else if (queueAccount) {
|
||||||
|
return OracleClass.fromJSON(context, queueAccount, definition);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`need to provide oracle queue account to build new oracle account`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
account: OracleAccount
|
||||||
|
): Promise<OracleClass> {
|
||||||
|
return OracleClass.init(context, account, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
) {
|
||||||
|
return OracleClass.init(
|
||||||
|
context,
|
||||||
|
new OracleAccount({
|
||||||
|
program,
|
||||||
|
publicKey,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fromJSON(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
definition: fromOracleJSON
|
||||||
|
) {
|
||||||
|
const account = await OracleAccount.create(queueAccount.program, {
|
||||||
|
queueAccount,
|
||||||
|
name: definition.name ? Buffer.from(definition.name) : Buffer.from(""),
|
||||||
|
oracleAuthority: definition.authorityKeypair,
|
||||||
|
metadata: definition.metadata
|
||||||
|
? Buffer.from(definition.metadata)
|
||||||
|
: Buffer.from(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
context.logger.info(
|
||||||
|
`created oracle account ${definition.name} ${account.publicKey}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return OracleClass.init(context, account, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromDefault(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
name = ""
|
||||||
|
): Promise<OracleClass> {
|
||||||
|
return OracleClass.build(
|
||||||
|
context,
|
||||||
|
queueAccount.program,
|
||||||
|
{ name },
|
||||||
|
queueAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getBalance(
|
||||||
|
oracleAccount: OracleAccount,
|
||||||
|
tokenAccount?: PublicKey,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<number> {
|
||||||
|
const oracleTokenAccount =
|
||||||
|
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||||
|
tokenAccount ?? (await oracleAccount.loadData()).tokenAccount;
|
||||||
|
const tokenAmount =
|
||||||
|
await oracleAccount.program.provider.connection.getTokenAccountBalance(
|
||||||
|
oracleTokenAccount
|
||||||
|
);
|
||||||
|
return Number.parseInt(tokenAmount.value.amount, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async withdrawTokens(
|
||||||
|
context: CommandContext,
|
||||||
|
oracleAccount: OracleAccount,
|
||||||
|
amount: number,
|
||||||
|
withdrawAccount: PublicKey,
|
||||||
|
authority?: Keypair,
|
||||||
|
force = false
|
||||||
|
): Promise<string> {
|
||||||
|
const { queuePubkey, tokenAccount, oracleAuthority } =
|
||||||
|
await oracleAccount.loadData();
|
||||||
|
|
||||||
|
const authorityKeypair =
|
||||||
|
authority || getProgramPayer(oracleAccount.program);
|
||||||
|
if (!oracleAuthority.equals(authorityKeypair.publicKey)) {
|
||||||
|
throw new Error(
|
||||||
|
`invalid oracle authority provided (expected) ${oracleAuthority}, (received) ${authority.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const oracleQueueAccount = new OracleQueueAccount({
|
||||||
|
program: oracleAccount.program,
|
||||||
|
publicKey: queuePubkey,
|
||||||
|
});
|
||||||
|
const oracleQueueData = await oracleQueueAccount.loadData();
|
||||||
|
const minStake: number = oracleQueueData.minStake.toNumber();
|
||||||
|
|
||||||
|
// check final balance is greater than min stake
|
||||||
|
const initialOracleBalance = await OracleClass.getBalance(
|
||||||
|
oracleAccount,
|
||||||
|
tokenAccount
|
||||||
|
);
|
||||||
|
const finalOracleBalance = initialOracleBalance - amount;
|
||||||
|
if (amount > initialOracleBalance) {
|
||||||
|
throw new Error(
|
||||||
|
`requested withdraw amount ${amount} exceeds current balance ${initialOracleBalance}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!force && minStake > finalOracleBalance)
|
||||||
|
throw new Error(
|
||||||
|
`withdrawing will result in your account falling below the minimum stake`
|
||||||
|
);
|
||||||
|
|
||||||
|
// withdraw
|
||||||
|
const withdrawTxn = await oracleAccount.withdraw({
|
||||||
|
amount: new anchor.BN(amount),
|
||||||
|
oracleAuthority: authorityKeypair,
|
||||||
|
withdrawAccount,
|
||||||
|
});
|
||||||
|
return withdrawTxn;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async depositTokens(
|
||||||
|
context: CommandContext,
|
||||||
|
oracleAccount: OracleAccount,
|
||||||
|
amount: number,
|
||||||
|
funderTokenAccount?: PublicKey
|
||||||
|
): Promise<string> {
|
||||||
|
const oracleTokenAccount =
|
||||||
|
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||||
|
(await oracleAccount.loadData()).tokenAccount;
|
||||||
|
const state = await ProgramStateClass.build(oracleAccount.program, context);
|
||||||
|
const payerTokenAccount =
|
||||||
|
funderTokenAccount ||
|
||||||
|
(await ProgramStateClass.getProgramTokenAddress(
|
||||||
|
oracleAccount.program,
|
||||||
|
context
|
||||||
|
));
|
||||||
|
|
||||||
|
// check payer has enough funds
|
||||||
|
const payerTokenBalance =
|
||||||
|
await oracleAccount.program.provider.connection.getBalance(
|
||||||
|
payerTokenAccount
|
||||||
|
);
|
||||||
|
if (amount > payerTokenBalance)
|
||||||
|
throw new Error(
|
||||||
|
`trying to deposit ${amount} tokens but current balance is ${payerTokenBalance}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.token.transfer(
|
||||||
|
payerTokenAccount,
|
||||||
|
oracleTokenAccount,
|
||||||
|
programWallet(oracleAccount.program),
|
||||||
|
[],
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: OracleAccountData = await this.account.loadData();
|
||||||
|
|
||||||
|
this.publicKey = this.account.publicKey;
|
||||||
|
this.name = buffer2string(data.name as any);
|
||||||
|
this.metadata = buffer2string(data.metadata as any);
|
||||||
|
this.authorityPublicKey = data.oracleAuthority;
|
||||||
|
this.lastHeartbeat = anchorBNtoDateTimeString(data.lastHeartbeat);
|
||||||
|
this.numInUse = data.numInUse;
|
||||||
|
this.tokenAccountPublicKey = data.tokenAccount;
|
||||||
|
this.queuePublicKey = data.queuePubkey;
|
||||||
|
this.metrics = data.metrics;
|
||||||
|
this.balance = await OracleClass.getBalance(
|
||||||
|
this.account,
|
||||||
|
this.tokenAccountPublicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): IOracleClass {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
metadata: this.metadata,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
authorityPublicKey: this.authorityPublicKey,
|
||||||
|
queuePublicKey: this.queuePublicKey,
|
||||||
|
tokenAccountPublicKey: this.tokenAccountPublicKey,
|
||||||
|
permissionAccount: this.permissionAccount.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false, SPACING = 24): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Oracle", this.publicKey.toString(), SPACING) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString += chalkString("name", this.name, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("metadata", this.metadata, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("balance", this.balance, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("oracleAuthority", this.authorityPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("tokenAccount", this.tokenAccountPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("queuePubkey", this.queuePublicKey, SPACING) + "\r\n";
|
||||||
|
if (this.permissionAccount) {
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"permissionAccount",
|
||||||
|
this.permissionAccount.publicKey || "N/A",
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"permissions",
|
||||||
|
this.permissionAccount.permission || "",
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
}
|
||||||
|
outputString +=
|
||||||
|
chalkString("lastHeartbeat", this.lastHeartbeat, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("numInUse", this.numInUse, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"metrics",
|
||||||
|
JSON.stringify(this.metrics, undefined, 2),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
|
||||||
|
if (all && this.permissionAccount) {
|
||||||
|
outputString += this.permissionAccount.prettyPrint(all, SPACING);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
fromPublicKey,
|
||||||
|
fromSwitchboardAccount,
|
||||||
|
IPermissionClass,
|
||||||
|
PermissionDefinition,
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
export interface OracleMetricsData {
|
||||||
|
consecutiveSuccess: anchor.BN;
|
||||||
|
consecutiveError: anchor.BN;
|
||||||
|
consecutiveDisagreement: anchor.BN;
|
||||||
|
consecutiveLateResponse: anchor.BN;
|
||||||
|
consecutiveFailure: anchor.BN;
|
||||||
|
totalSuccess: anchor.BN;
|
||||||
|
totalError: anchor.BN;
|
||||||
|
totalDisagreement: anchor.BN;
|
||||||
|
totalLateResponse: anchor.BN;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OracleAccountData {
|
||||||
|
name: Buffer;
|
||||||
|
metadata: Buffer;
|
||||||
|
oracleAuthority: PublicKey;
|
||||||
|
lastHeartbeat: anchor.BN;
|
||||||
|
numInUse: number;
|
||||||
|
tokenAccount: PublicKey;
|
||||||
|
queuePubkey: PublicKey;
|
||||||
|
metrics: OracleMetricsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON interface to construct a new Oracle Account */
|
||||||
|
export interface fromOracleJSON {
|
||||||
|
name?: string;
|
||||||
|
metadata?: string;
|
||||||
|
queuePublicKey?: PublicKey;
|
||||||
|
tokenAccountPublicKey?: PublicKey;
|
||||||
|
authorityKeypair?: Keypair;
|
||||||
|
permissionAccount?: PermissionDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Oracle Account */
|
||||||
|
export interface IOracleClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
name: string;
|
||||||
|
metadata: string;
|
||||||
|
queuePublicKey?: PublicKey;
|
||||||
|
tokenAccountPublicKey?: PublicKey;
|
||||||
|
authorityPublicKey?: PublicKey;
|
||||||
|
permissionAccount?: IPermissionClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type representing the different ways to build an Oracle Account */
|
||||||
|
export type OracleDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| fromOracleJSON;
|
||||||
|
|
||||||
|
/** Type representing the different ways to build a set of Oracle Accounts */
|
||||||
|
export type OracleDefinitions = OracleDefinition[] | number;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./permission";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,304 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
OracleAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
PermissionAccount,
|
||||||
|
SwitchboardPermission,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { CommandContext } from "../../types/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { programHasPayer } from "../../utils";
|
||||||
|
import { AggregatorAccountData } from "../aggregator";
|
||||||
|
import { OracleAccountData } from "../oracle";
|
||||||
|
import {
|
||||||
|
anchorBNtoDateTimeString,
|
||||||
|
chalkString,
|
||||||
|
pubKeyConverter,
|
||||||
|
toPermissionString,
|
||||||
|
} from "../utils";
|
||||||
|
import {
|
||||||
|
IPermissionClass,
|
||||||
|
PermissionAccountData,
|
||||||
|
PermissionDefinition,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class PermissionClass implements IPermissionClass {
|
||||||
|
account: PermissionAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
|
||||||
|
granterPublicKey: PublicKey;
|
||||||
|
|
||||||
|
granteePublicKey: PublicKey;
|
||||||
|
|
||||||
|
permission: string;
|
||||||
|
|
||||||
|
expiration: anchor.BN;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private static async init(
|
||||||
|
context: CommandContext,
|
||||||
|
account: PermissionAccount
|
||||||
|
) {
|
||||||
|
const permission = new PermissionClass();
|
||||||
|
permission.logger = context.logger;
|
||||||
|
|
||||||
|
permission.account = account;
|
||||||
|
permission.publicKey = permission.account.publicKey;
|
||||||
|
|
||||||
|
await permission.loadData();
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async build(
|
||||||
|
context: CommandContext,
|
||||||
|
granteeAccount: AggregatorAccount | OracleAccount,
|
||||||
|
queueAccount?: OracleQueueAccount,
|
||||||
|
definition?: PermissionDefinition
|
||||||
|
): Promise<PermissionClass | undefined> {
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
|
if (definition && "account" in definition) {
|
||||||
|
if (definition.account instanceof PermissionAccount) {
|
||||||
|
return PermissionClass.fromAccount(context, definition.account);
|
||||||
|
}
|
||||||
|
throw new TypeError("account must be an instance of PermissionAccount");
|
||||||
|
} else if (definition && "publicKey" in definition) {
|
||||||
|
return PermissionClass.fromPublicKey(
|
||||||
|
context,
|
||||||
|
granteeAccount.program,
|
||||||
|
definition.publicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programHasPayer(granteeAccount.program)) {
|
||||||
|
return PermissionClass.getOrCreatePermissionAccount(
|
||||||
|
context,
|
||||||
|
granteeAccount,
|
||||||
|
queueAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return PermissionClass.getPermissionAccount(
|
||||||
|
context,
|
||||||
|
granteeAccount,
|
||||||
|
queueAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
account: PermissionAccount
|
||||||
|
) {
|
||||||
|
return PermissionClass.init(context, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
) {
|
||||||
|
return PermissionClass.init(
|
||||||
|
context,
|
||||||
|
new PermissionAccount({
|
||||||
|
program,
|
||||||
|
publicKey,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async grantPermission(
|
||||||
|
context: CommandContext,
|
||||||
|
granteeAccount: OracleAccount | AggregatorAccount,
|
||||||
|
granterAuthority: Keypair
|
||||||
|
): Promise<PermissionClass> {
|
||||||
|
const permission: PermissionClass | undefined = programHasPayer(
|
||||||
|
granteeAccount.program
|
||||||
|
)
|
||||||
|
? await PermissionClass.getOrCreatePermissionAccount(
|
||||||
|
context,
|
||||||
|
granteeAccount
|
||||||
|
)
|
||||||
|
: await PermissionClass.getPermissionAccount(context, granteeAccount);
|
||||||
|
if (permission === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`no payer provided and no existing permission account found for ${granteeAccount.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permission.authorityPublicKey.equals(granterAuthority.publicKey)) {
|
||||||
|
throw new Error(
|
||||||
|
`wrong authority provided to grant permission, expected ${permission.authorityPublicKey}, received ${granterAuthority.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (granteeAccount instanceof AggregatorAccount) {
|
||||||
|
await permission.account.set({
|
||||||
|
authority: granterAuthority,
|
||||||
|
enable: true,
|
||||||
|
permission: SwitchboardPermission.PERMIT_ORACLE_QUEUE_USAGE,
|
||||||
|
});
|
||||||
|
await permission.loadData();
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
if (granteeAccount instanceof OracleAccount) {
|
||||||
|
await permission.account.set({
|
||||||
|
authority: granterAuthority,
|
||||||
|
enable: true,
|
||||||
|
permission: SwitchboardPermission.PERMIT_ORACLE_HEARTBEAT,
|
||||||
|
});
|
||||||
|
await permission.loadData();
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`permission grantee account isnt an aggregator or oracle account`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getPermissionAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
granteeAccount: OracleAccount | AggregatorAccount,
|
||||||
|
oracleQueueAccount?: OracleQueueAccount
|
||||||
|
): Promise<PermissionClass | undefined> {
|
||||||
|
let queueAccount = oracleQueueAccount;
|
||||||
|
if (!queueAccount) {
|
||||||
|
const data: AggregatorAccountData | OracleAccountData =
|
||||||
|
await granteeAccount.loadData();
|
||||||
|
queueAccount = new OracleQueueAccount({
|
||||||
|
program: granteeAccount.program,
|
||||||
|
publicKey: data.queuePubkey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueAuthority: anchor.web3.PublicKey =
|
||||||
|
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||||
|
new PublicKey((await queueAccount.loadData()).authority);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [permissionAccount] = PermissionAccount.fromSeed(
|
||||||
|
granteeAccount.program,
|
||||||
|
queueAuthority,
|
||||||
|
queueAccount.publicKey,
|
||||||
|
granteeAccount.publicKey
|
||||||
|
);
|
||||||
|
await permissionAccount.loadData();
|
||||||
|
context.logger.debug(
|
||||||
|
`loaded permission account ${permissionAccount.publicKey} from seed for ${granteeAccount.publicKey}`
|
||||||
|
);
|
||||||
|
return await PermissionClass.init(context, permissionAccount);
|
||||||
|
} catch {}
|
||||||
|
context.logger.debug(
|
||||||
|
`no permission account found for ${granteeAccount.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getOrCreatePermissionAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
granteeAccount: OracleAccount | AggregatorAccount,
|
||||||
|
oracleQueueAccount?: OracleQueueAccount
|
||||||
|
): Promise<PermissionClass> {
|
||||||
|
let queueAccount = oracleQueueAccount;
|
||||||
|
if (!queueAccount) {
|
||||||
|
const data: AggregatorAccountData | OracleAccountData =
|
||||||
|
await granteeAccount.loadData();
|
||||||
|
queueAccount = new OracleQueueAccount({
|
||||||
|
program: granteeAccount.program,
|
||||||
|
publicKey: data.queuePubkey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueAuthority: anchor.web3.PublicKey =
|
||||||
|
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||||
|
new PublicKey((await queueAccount.loadData()).authority);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [permissionAccount] = PermissionAccount.fromSeed(
|
||||||
|
granteeAccount.program,
|
||||||
|
queueAuthority,
|
||||||
|
queueAccount.publicKey,
|
||||||
|
granteeAccount.publicKey
|
||||||
|
);
|
||||||
|
await permissionAccount.loadData();
|
||||||
|
context.logger.debug(
|
||||||
|
`loaded permission account ${permissionAccount.publicKey} from seed for ${granteeAccount.publicKey}`
|
||||||
|
);
|
||||||
|
return await PermissionClass.init(context, permissionAccount);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const permissionAccount = await PermissionAccount.create(
|
||||||
|
queueAccount.program,
|
||||||
|
{
|
||||||
|
authority: queueAuthority,
|
||||||
|
grantee: granteeAccount.publicKey,
|
||||||
|
granter: queueAccount.publicKey,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await permissionAccount.loadData();
|
||||||
|
context.logger.debug(
|
||||||
|
`created new permission account ${permissionAccount.publicKey} for ${granteeAccount.publicKey}`
|
||||||
|
);
|
||||||
|
return PermissionClass.init(context, permissionAccount);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`failed to create permission account ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: PermissionAccountData = await this.account.loadData();
|
||||||
|
|
||||||
|
this.publicKey = this.account.publicKey;
|
||||||
|
this.permission = toPermissionString(data.permissions);
|
||||||
|
this.authorityPublicKey = data.authority;
|
||||||
|
this.expiration = data.expiration;
|
||||||
|
this.granterPublicKey = data.granter;
|
||||||
|
this.granteePublicKey = data.grantee;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): IPermissionClass {
|
||||||
|
return {
|
||||||
|
publicKey: this.account.publicKey,
|
||||||
|
permission: this.permission,
|
||||||
|
authorityPublicKey: this.authorityPublicKey,
|
||||||
|
expiration: this.expiration,
|
||||||
|
granterPublicKey: this.granterPublicKey,
|
||||||
|
granteePublicKey: this.granteePublicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false, SPACING = 24): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Permission", this.publicKey, SPACING) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString +=
|
||||||
|
chalkString("authority", this.authorityPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("permissions", this.permission, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("granter", this.granterPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("grantee", this.granteePublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"expiration",
|
||||||
|
anchorBNtoDateTimeString(this.expiration),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { SwitchboardPermissionValue } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import { fromPublicKey, fromSwitchboardAccount } from "..";
|
||||||
|
|
||||||
|
export interface PermissionAccountData {
|
||||||
|
authority: PublicKey;
|
||||||
|
permissions: SwitchboardPermissionValue;
|
||||||
|
granter: PublicKey;
|
||||||
|
grantee: PublicKey;
|
||||||
|
expiration: anchor.BN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Permission Account */
|
||||||
|
export interface IPermissionClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
granterPublicKey: PublicKey;
|
||||||
|
granteePublicKey: PublicKey;
|
||||||
|
permission: string;
|
||||||
|
expiration: anchor.BN;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermissionDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| IPermissionClass;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./queue";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,540 @@
|
||||||
|
/* eslint-disable max-depth */
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import * as spl from "@solana/spl-token";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
SwitchboardDecimal,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { fromPublicKey, fromQueueJSON } from "..";
|
||||||
|
import { CommandContext } from "../../types/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { AggregatorClass, AggregatorDefinition } from "../aggregator";
|
||||||
|
import { CrankClass, CrankDefinition, CrankDefinitions } from "../crank";
|
||||||
|
import { OracleClass, OracleDefinition, OracleDefinitions } from "../oracle";
|
||||||
|
import {
|
||||||
|
buffer2string,
|
||||||
|
chalkString,
|
||||||
|
getArrayOfSizeN,
|
||||||
|
pubKeyConverter,
|
||||||
|
} from "../utils";
|
||||||
|
import {
|
||||||
|
IOracleQueueClass,
|
||||||
|
OracleQueueAccountData,
|
||||||
|
QueueDefinition,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class OracleQueueClass implements IOracleQueueClass {
|
||||||
|
account: OracleQueueAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
|
||||||
|
consecutiveFeedFailureLimit: anchor.BN;
|
||||||
|
|
||||||
|
consecutiveOracleFailureLimit: anchor.BN;
|
||||||
|
|
||||||
|
feedProbationPeriod: number;
|
||||||
|
|
||||||
|
metadata: string;
|
||||||
|
|
||||||
|
minStake: anchor.BN;
|
||||||
|
|
||||||
|
minUpdateDelaySeconds: number;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
oracleTimeout: anchor.BN;
|
||||||
|
|
||||||
|
queueSize: number;
|
||||||
|
|
||||||
|
reward: anchor.BN;
|
||||||
|
|
||||||
|
slashingEnabled: boolean;
|
||||||
|
|
||||||
|
unpermissionedFeedsEnabled: boolean;
|
||||||
|
|
||||||
|
unpermissionedVrfEnabled: boolean;
|
||||||
|
|
||||||
|
varianceToleranceMultiplier: SwitchboardDecimal;
|
||||||
|
|
||||||
|
oracleBuffer: PublicKey;
|
||||||
|
|
||||||
|
cranks: CrankClass[] = [];
|
||||||
|
|
||||||
|
oracles: OracleClass[] = [];
|
||||||
|
|
||||||
|
aggregators: AggregatorClass[] = [];
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private static async init(
|
||||||
|
context: CommandContext,
|
||||||
|
account: OracleQueueAccount,
|
||||||
|
definition: QueueDefinition
|
||||||
|
) {
|
||||||
|
const queue = new OracleQueueClass();
|
||||||
|
queue.logger = context.logger;
|
||||||
|
|
||||||
|
queue.account = account;
|
||||||
|
queue.publicKey = queue.account.publicKey;
|
||||||
|
|
||||||
|
await queue.loadData();
|
||||||
|
|
||||||
|
if (definition && "oracles" in definition) {
|
||||||
|
queue.oracles = await OracleQueueClass.buildOracles(
|
||||||
|
context,
|
||||||
|
queue.account,
|
||||||
|
definition.oracles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition && "cranks" in definition) {
|
||||||
|
queue.cranks = await OracleQueueClass.buildCranks(
|
||||||
|
context,
|
||||||
|
queue.account,
|
||||||
|
definition.cranks
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition && "aggregators" in definition) {
|
||||||
|
queue.logger.debug(
|
||||||
|
`creating ${definition.aggregators.length} aggregators from json definition`
|
||||||
|
);
|
||||||
|
for await (const aggregatorDefinition of definition.aggregators) {
|
||||||
|
const aggregator = await AggregatorClass.build(
|
||||||
|
context,
|
||||||
|
queue.account.program,
|
||||||
|
aggregatorDefinition,
|
||||||
|
queue.account
|
||||||
|
);
|
||||||
|
|
||||||
|
await aggregator.grantPermission(context);
|
||||||
|
queue.aggregators.push(aggregator);
|
||||||
|
|
||||||
|
if (
|
||||||
|
"crank" in aggregatorDefinition &&
|
||||||
|
queue.cranks &&
|
||||||
|
queue.cranks.length > 0
|
||||||
|
) {
|
||||||
|
const key = aggregatorDefinition.crank;
|
||||||
|
const crank = await OracleQueueClass.findCrankByKey(
|
||||||
|
context,
|
||||||
|
queue.cranks,
|
||||||
|
key
|
||||||
|
);
|
||||||
|
if (typeof key === "object" && !("publicKey" in key)) {
|
||||||
|
crank.account.push({
|
||||||
|
aggregatorAccount: aggregator.account,
|
||||||
|
});
|
||||||
|
context.logger.debug(
|
||||||
|
`added aggregator ${aggregator.name} to crank ${key} ${crank.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregator.crankPublicKey = crank.publicKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async build(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: QueueDefinition
|
||||||
|
): Promise<OracleQueueClass> {
|
||||||
|
if ("account" in definition) {
|
||||||
|
if (definition.account instanceof OracleQueueAccount) {
|
||||||
|
return OracleQueueClass.fromAccount(context, definition.account);
|
||||||
|
}
|
||||||
|
const errorMessage = `found account key in definition file but account is not an instanceof OracleQueueAccount`;
|
||||||
|
context.logger.error(errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
} else if ("publicKey" in definition) {
|
||||||
|
return OracleQueueClass.fromPublicKey(context, program, definition);
|
||||||
|
} else if ("name" in definition) {
|
||||||
|
return OracleQueueClass.fromJSON(context, program, definition);
|
||||||
|
}
|
||||||
|
throw new Error("failed to build QueueClass");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromAccount(
|
||||||
|
context: CommandContext,
|
||||||
|
account: OracleQueueAccount
|
||||||
|
) {
|
||||||
|
return OracleQueueClass.init(context, account, { name: "queue" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromPublicKey(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: fromPublicKey
|
||||||
|
) {
|
||||||
|
return OracleQueueClass.init(
|
||||||
|
context,
|
||||||
|
new OracleQueueAccount({
|
||||||
|
program,
|
||||||
|
publicKey: definition.publicKey,
|
||||||
|
}),
|
||||||
|
definition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fromJSON(
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
definition: fromQueueJSON
|
||||||
|
) {
|
||||||
|
const payer = programWallet(program);
|
||||||
|
const queueAccount = await OracleQueueAccount.create(program, {
|
||||||
|
authority: definition.authorityPublicKey ?? payer.publicKey,
|
||||||
|
name: definition.name ? Buffer.from(definition.name) : undefined,
|
||||||
|
metadata: definition.metadata
|
||||||
|
? Buffer.from(definition.metadata)
|
||||||
|
: undefined,
|
||||||
|
reward: definition.reward
|
||||||
|
? new anchor.BN(definition.reward)
|
||||||
|
: new anchor.BN(0),
|
||||||
|
minStake: definition.minStake
|
||||||
|
? new anchor.BN(definition.minStake)
|
||||||
|
: new anchor.BN(0),
|
||||||
|
minimumDelaySeconds: definition.minUpdateDelaySeconds,
|
||||||
|
oracleTimeout: definition.oracleTimeout
|
||||||
|
? new anchor.BN(definition.oracleTimeout)
|
||||||
|
: undefined,
|
||||||
|
slashingEnabled: definition.slashingEnabled,
|
||||||
|
unpermissionedFeeds: definition.unpermissionedFeedsEnabled,
|
||||||
|
feedProbationPeriod: definition.feedProbationPeriod,
|
||||||
|
consecutiveFeedFailureLimit: definition.consecutiveFeedFailureLimit
|
||||||
|
? new anchor.BN(definition.consecutiveFeedFailureLimit)
|
||||||
|
: undefined,
|
||||||
|
consecutiveOracleFailureLimit: definition.consecutiveOracleFailureLimit
|
||||||
|
? new anchor.BN(definition.consecutiveOracleFailureLimit)
|
||||||
|
: undefined,
|
||||||
|
varianceToleranceMultiplier: definition.varianceToleranceMultiplier,
|
||||||
|
mint: spl.NATIVE_MINT,
|
||||||
|
});
|
||||||
|
|
||||||
|
context.logger.info(`created queue ${queueAccount.publicKey}`);
|
||||||
|
|
||||||
|
return OracleQueueClass.init(context, queueAccount, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCrank(
|
||||||
|
context: CommandContext,
|
||||||
|
crankDefinition: CrankDefinition
|
||||||
|
): Promise<number> {
|
||||||
|
const crank = await CrankClass.fromJSON(
|
||||||
|
context,
|
||||||
|
{
|
||||||
|
...crankDefinition,
|
||||||
|
queuePublicKey: this.publicKey,
|
||||||
|
},
|
||||||
|
this.account
|
||||||
|
);
|
||||||
|
this.cranks.push(crank);
|
||||||
|
const newCrankIndex = this.cranks.findIndex((c) =>
|
||||||
|
c.publicKey.equals(crank.publicKey)
|
||||||
|
);
|
||||||
|
if (newCrankIndex === -1) {
|
||||||
|
throw new Error(`failed to find new crank in queue`);
|
||||||
|
}
|
||||||
|
return newCrankIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async buildCranks(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
cranks: CrankDefinitions
|
||||||
|
): Promise<CrankClass[]> {
|
||||||
|
const newCranks: CrankClass[] = [];
|
||||||
|
if (typeof cranks === "number") {
|
||||||
|
context.logger.debug(`creating ${cranks} cranks`);
|
||||||
|
for await (const index of getArrayOfSizeN(cranks)) {
|
||||||
|
const crank = await CrankClass.fromDefault(
|
||||||
|
context,
|
||||||
|
queueAccount,
|
||||||
|
`Crank-${index}` // should we get last crank num and increment?
|
||||||
|
);
|
||||||
|
newCranks.push(crank);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.logger.debug(
|
||||||
|
`creating ${cranks.length} cranks from json definition`
|
||||||
|
);
|
||||||
|
for await (const crankDefinition of cranks) {
|
||||||
|
const crank = await CrankClass.fromJSON(
|
||||||
|
context,
|
||||||
|
{
|
||||||
|
...crankDefinition,
|
||||||
|
queuePublicKey: queueAccount.publicKey,
|
||||||
|
},
|
||||||
|
queueAccount
|
||||||
|
);
|
||||||
|
newCranks.push(crank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newCranks;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addOracle(
|
||||||
|
context: CommandContext,
|
||||||
|
oracleDefinition: OracleDefinition
|
||||||
|
): Promise<number> {
|
||||||
|
const oracle = await OracleClass.build(
|
||||||
|
context,
|
||||||
|
this.account.program,
|
||||||
|
{
|
||||||
|
...oracleDefinition,
|
||||||
|
queuePublicKey: this.publicKey,
|
||||||
|
},
|
||||||
|
this.account
|
||||||
|
);
|
||||||
|
|
||||||
|
this.oracles.push(oracle);
|
||||||
|
const newOracleIndex = this.oracles.findIndex((o) =>
|
||||||
|
o.publicKey.equals(oracle.publicKey)
|
||||||
|
);
|
||||||
|
if (newOracleIndex === -1) {
|
||||||
|
throw new Error(`failed to find new oracle in queue`);
|
||||||
|
}
|
||||||
|
return newOracleIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async buildOracles(
|
||||||
|
context: CommandContext,
|
||||||
|
queueAccount: OracleQueueAccount,
|
||||||
|
oracles: OracleDefinitions
|
||||||
|
): Promise<OracleClass[]> {
|
||||||
|
const newOracles: OracleClass[] = [];
|
||||||
|
if (typeof oracles === "number") {
|
||||||
|
context.logger.debug(`creating ${oracles} oracles`);
|
||||||
|
for await (const index of getArrayOfSizeN(oracles)) {
|
||||||
|
const oracle = await OracleClass.fromDefault(
|
||||||
|
context,
|
||||||
|
queueAccount,
|
||||||
|
`Oracle-${index}`
|
||||||
|
);
|
||||||
|
await oracle.grantPermission(context);
|
||||||
|
newOracles.push(oracle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.logger.debug(
|
||||||
|
`creating ${oracles.length} oracles from json definition`
|
||||||
|
);
|
||||||
|
for await (const oracleDefinition of oracles) {
|
||||||
|
const oracle = await OracleClass.build(
|
||||||
|
context,
|
||||||
|
queueAccount.program,
|
||||||
|
{
|
||||||
|
...oracleDefinition,
|
||||||
|
queuePublicKey: queueAccount.publicKey,
|
||||||
|
},
|
||||||
|
queueAccount
|
||||||
|
);
|
||||||
|
await oracle.grantPermission(context);
|
||||||
|
newOracles.push(oracle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newOracles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAggregator(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregatorDefinition: AggregatorDefinition
|
||||||
|
): Promise<number> {
|
||||||
|
const aggregator = await AggregatorClass.build(
|
||||||
|
context,
|
||||||
|
this.account.program,
|
||||||
|
aggregatorDefinition,
|
||||||
|
this.account
|
||||||
|
);
|
||||||
|
await aggregator.grantPermission(context);
|
||||||
|
this.aggregators.push(aggregator);
|
||||||
|
const newAggregatorIndex = this.aggregators.findIndex((aggregator) =>
|
||||||
|
aggregator.publicKey.equals(aggregator.publicKey)
|
||||||
|
);
|
||||||
|
if (newAggregatorIndex === -1) {
|
||||||
|
throw new Error(`failed to find new aggregator in queue`);
|
||||||
|
}
|
||||||
|
return newAggregatorIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async findCrankByKey(
|
||||||
|
context: CommandContext,
|
||||||
|
cranks: CrankClass[],
|
||||||
|
key: string | number | boolean | PublicKey
|
||||||
|
): Promise<CrankClass> {
|
||||||
|
if (typeof key === "number") {
|
||||||
|
if (cranks.length >= key) {
|
||||||
|
const crank = cranks[key];
|
||||||
|
return crank;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`failed to find crank by key ${key}, length ${cranks.length}`
|
||||||
|
);
|
||||||
|
} else if (typeof key === "boolean") {
|
||||||
|
if (cranks.length > 0) {
|
||||||
|
const crank = cranks[0];
|
||||||
|
return crank;
|
||||||
|
}
|
||||||
|
throw new Error(`failed to find any cranks`);
|
||||||
|
} else if (typeof key === "string") {
|
||||||
|
const foundCrank = cranks.find((c) => c.name === key);
|
||||||
|
if (foundCrank) {
|
||||||
|
return foundCrank;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const crankKey = new PublicKey(key);
|
||||||
|
const crank = cranks.find((c) => c.publicKey === crankKey);
|
||||||
|
if (crank) {
|
||||||
|
return crank;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addAggregatorToCrank(
|
||||||
|
context: CommandContext,
|
||||||
|
aggregator: AggregatorClass,
|
||||||
|
cranks: CrankClass[],
|
||||||
|
key: string | number | boolean
|
||||||
|
) {
|
||||||
|
const crank = await OracleQueueClass.findCrankByKey(context, cranks, key);
|
||||||
|
crank.account.push({
|
||||||
|
aggregatorAccount: aggregator.account,
|
||||||
|
});
|
||||||
|
context.logger.debug(
|
||||||
|
`added aggregator ${aggregator.name} to crank ${key} ${crank.publicKey}`
|
||||||
|
);
|
||||||
|
aggregator.crankPublicKey = crank.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: OracleQueueAccountData = await this.account.loadData();
|
||||||
|
this.name = buffer2string(data.name as any);
|
||||||
|
this.metadata = buffer2string(data.metadata as any);
|
||||||
|
this.authorityPublicKey = data.authority;
|
||||||
|
this.oracleTimeout = data.oracleTimeout;
|
||||||
|
this.reward = data.reward;
|
||||||
|
this.minStake = data.minStake;
|
||||||
|
this.slashingEnabled = data.slashingEnabled;
|
||||||
|
this.varianceToleranceMultiplier = data.varianceToleranceMultiplier;
|
||||||
|
this.feedProbationPeriod = data.feedProbationPeriod;
|
||||||
|
this.consecutiveFeedFailureLimit = data.consecutiveFeedFailureLimit;
|
||||||
|
this.consecutiveOracleFailureLimit = data.consecutiveOracleFailureLimit;
|
||||||
|
this.unpermissionedFeedsEnabled = data.unpermissionedFeedsEnabled;
|
||||||
|
this.unpermissionedVrfEnabled = data.unpermissionedVrfEnabled;
|
||||||
|
this.oracleBuffer = data.dataBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// needed to enforce ordering of json output
|
||||||
|
toJSON(): IOracleQueueClass {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
metadata: this.metadata,
|
||||||
|
publicKey: this.publicKey,
|
||||||
|
authorityPublicKey: this.authorityPublicKey,
|
||||||
|
queueSize: this.queueSize,
|
||||||
|
minStake: this.minStake,
|
||||||
|
reward: this.reward,
|
||||||
|
slashingEnabled: this.slashingEnabled,
|
||||||
|
unpermissionedFeedsEnabled: this.unpermissionedFeedsEnabled,
|
||||||
|
unpermissionedVrfEnabled: this.unpermissionedVrfEnabled,
|
||||||
|
minUpdateDelaySeconds: this.minUpdateDelaySeconds,
|
||||||
|
consecutiveFeedFailureLimit: this.consecutiveFeedFailureLimit,
|
||||||
|
feedProbationPeriod: this.feedProbationPeriod,
|
||||||
|
consecutiveOracleFailureLimit: this.consecutiveOracleFailureLimit,
|
||||||
|
oracleTimeout: this.oracleTimeout,
|
||||||
|
varianceToleranceMultiplier: this.varianceToleranceMultiplier,
|
||||||
|
cranks: this.cranks ? this.cranks.map((crank) => crank.toJSON()) : [],
|
||||||
|
oracles: this.oracles
|
||||||
|
? this.oracles.map((oracle) => oracle.toJSON())
|
||||||
|
: [],
|
||||||
|
aggregators: this.aggregators
|
||||||
|
? this.aggregators.map((aggregator) => aggregator.toJSON())
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(all = false, SPACING = 30): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Queue", this.account.publicKey, SPACING) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString += chalkString("name", this.name, SPACING) + "\r\n";
|
||||||
|
outputString += chalkString("metadata", this.metadata, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("oracleBuffer", this.oracleBuffer, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("authority", this.authorityPublicKey, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("oracleTimeout", this.oracleTimeout.toString(), SPACING) +
|
||||||
|
"\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("reward", this.reward.toString(), SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("minStake", this.minStake.toString(), SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("slashingEnabled", this.slashingEnabled, SPACING) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"consecutiveFeedFailureLimit",
|
||||||
|
this.consecutiveFeedFailureLimit.toString(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"consecutiveOracleFailureLimit",
|
||||||
|
this.consecutiveOracleFailureLimit.toString(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
// outputString += chalkString(
|
||||||
|
// "varianceToleranceMultiplier",
|
||||||
|
// this.varianceToleranceMultiplier.toBig().toString(),
|
||||||
|
// SPACING
|
||||||
|
// ) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"feedProbationPeriod",
|
||||||
|
this.feedProbationPeriod.toString(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"unpermissionedFeedsEnabled",
|
||||||
|
this.unpermissionedFeedsEnabled.toString(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString(
|
||||||
|
"unpermissionedVrfEnabled",
|
||||||
|
this.unpermissionedVrfEnabled.toString(),
|
||||||
|
SPACING
|
||||||
|
) + "\r\n";
|
||||||
|
|
||||||
|
if (all) {
|
||||||
|
for (const crank of this.cranks) {
|
||||||
|
outputString += crank.prettyPrint(true, SPACING);
|
||||||
|
}
|
||||||
|
for (const oracle of this.oracles) {
|
||||||
|
outputString += oracle.prettyPrint(true, SPACING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { SwitchboardDecimal } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import {
|
||||||
|
AggregatorDefinition,
|
||||||
|
CrankDefinitions,
|
||||||
|
fromPublicKey,
|
||||||
|
fromSwitchboardAccount,
|
||||||
|
IAggregatorClass,
|
||||||
|
ICrankClass,
|
||||||
|
IOracleClass,
|
||||||
|
OracleDefinitions,
|
||||||
|
} from "..";
|
||||||
|
|
||||||
|
export interface OracleQueueAccountData {
|
||||||
|
name: Buffer;
|
||||||
|
metadata: Buffer;
|
||||||
|
authority: PublicKey;
|
||||||
|
oracleTimeout: anchor.BN;
|
||||||
|
reward: anchor.BN;
|
||||||
|
minStake: anchor.BN;
|
||||||
|
slashingEnabled: boolean;
|
||||||
|
varianceToleranceMultiplier: SwitchboardDecimal;
|
||||||
|
feedProbationPeriod: number;
|
||||||
|
currIdx: number;
|
||||||
|
size: number;
|
||||||
|
gcIdx: number;
|
||||||
|
consecutiveFeedFailureLimit: anchor.BN;
|
||||||
|
consecutiveOracleFailureLimit: anchor.BN;
|
||||||
|
unpermissionedFeedsEnabled: boolean;
|
||||||
|
unpermissionedVrfEnabled: boolean;
|
||||||
|
maxSize: number;
|
||||||
|
dataBuffer: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON interface to construct a new Oracle Queue Account */
|
||||||
|
export interface fromQueueJSON {
|
||||||
|
authorityPublicKey?: PublicKey;
|
||||||
|
consecutiveFeedFailureLimit?: string | number; // BN
|
||||||
|
consecutiveOracleFailureLimit?: string | number; // BN
|
||||||
|
feedProbationPeriod?: number;
|
||||||
|
metadata?: string; // Buffer
|
||||||
|
minStake?: string | number; // BN
|
||||||
|
minUpdateDelaySeconds?: number;
|
||||||
|
// queues should be named for easy lookup
|
||||||
|
name: string; // Buffer
|
||||||
|
oracleTimeout?: string | number; // BN
|
||||||
|
queueSize?: number;
|
||||||
|
reward?: string | number; // BN
|
||||||
|
slashingEnabled?: boolean;
|
||||||
|
unpermissionedFeedsEnabled?: boolean;
|
||||||
|
unpermissionedVrfEnabled?: boolean;
|
||||||
|
varianceToleranceMultiplier?: number;
|
||||||
|
// accounts
|
||||||
|
cranks?: CrankDefinitions; // can have crank-less queues
|
||||||
|
oracles?: OracleDefinitions;
|
||||||
|
aggregators?: AggregatorDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Object representing a loaded onchain Oracle Queue Account */
|
||||||
|
export interface IOracleQueueClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
consecutiveFeedFailureLimit: anchor.BN;
|
||||||
|
consecutiveOracleFailureLimit: anchor.BN;
|
||||||
|
feedProbationPeriod: number;
|
||||||
|
metadata: string; // Buffer
|
||||||
|
minStake: anchor.BN;
|
||||||
|
minUpdateDelaySeconds: number;
|
||||||
|
// queues should be named for easy lookup
|
||||||
|
name: string; // Buffer
|
||||||
|
oracleTimeout: anchor.BN;
|
||||||
|
queueSize: number;
|
||||||
|
reward: anchor.BN;
|
||||||
|
slashingEnabled: boolean;
|
||||||
|
unpermissionedFeedsEnabled?: boolean;
|
||||||
|
unpermissionedVrfEnabled?: boolean;
|
||||||
|
varianceToleranceMultiplier: SwitchboardDecimal;
|
||||||
|
// accounts
|
||||||
|
cranks?: ICrankClass[];
|
||||||
|
oracles?: IOracleClass[];
|
||||||
|
aggregators?: IAggregatorClass[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueueDefinition =
|
||||||
|
| fromSwitchboardAccount
|
||||||
|
| fromPublicKey
|
||||||
|
| fromQueueJSON
|
||||||
|
| IOracleQueueClass;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./state";
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,131 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { MintInfo, Token } from "@solana/spl-token";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
ProgramStateAccount,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { DEFAULT_CONTEXT } from "../../types/context/context";
|
||||||
|
import { LogProvider } from "../../types/context/logging";
|
||||||
|
import { chalkString, pubKeyConverter } from "../utils";
|
||||||
|
import { IProgramStateClass, ProgramStateData } from "./types";
|
||||||
|
|
||||||
|
export class ProgramStateClass implements IProgramStateClass {
|
||||||
|
account: ProgramStateAccount;
|
||||||
|
|
||||||
|
logger: LogProvider;
|
||||||
|
|
||||||
|
publicKey: PublicKey;
|
||||||
|
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
|
||||||
|
tokenMintPublicKey: PublicKey;
|
||||||
|
|
||||||
|
tokenVaultPublicKey: PublicKey;
|
||||||
|
|
||||||
|
token: Token;
|
||||||
|
|
||||||
|
mintInfo: MintInfo;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static async build(
|
||||||
|
program: anchor.Program,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<ProgramStateClass> {
|
||||||
|
const state = new ProgramStateClass();
|
||||||
|
state.logger = context.logger;
|
||||||
|
|
||||||
|
// eslint-disable-next-line unicorn/prefer-ternary
|
||||||
|
[state.account] = ProgramStateAccount.fromSeed(program);
|
||||||
|
|
||||||
|
await state.loadData();
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads anchor idl and parses response
|
||||||
|
async loadData() {
|
||||||
|
const data: ProgramStateData = await this.account.loadData();
|
||||||
|
|
||||||
|
this.publicKey = this.account.publicKey;
|
||||||
|
|
||||||
|
this.authorityPublicKey = data.authority;
|
||||||
|
this.tokenMintPublicKey = data.tokenMint;
|
||||||
|
this.tokenVaultPublicKey = data.tokenVault;
|
||||||
|
|
||||||
|
this.token = await this.account.getTokenMint();
|
||||||
|
this.mintInfo = await this.token.getMintInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAssociatedTokenAddress(
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<PublicKey> {
|
||||||
|
const state = await ProgramStateClass.build(program, context);
|
||||||
|
const account = await state.token.getOrCreateAssociatedAccountInfo(
|
||||||
|
publicKey
|
||||||
|
);
|
||||||
|
return account.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getProgramTokenAddress(
|
||||||
|
program: anchor.Program,
|
||||||
|
context = DEFAULT_CONTEXT
|
||||||
|
): Promise<PublicKey> {
|
||||||
|
const state = await ProgramStateClass.build(program, context);
|
||||||
|
const wallet = programWallet(program);
|
||||||
|
const account = await state.token.getOrCreateAssociatedAccountInfo(
|
||||||
|
wallet.publicKey
|
||||||
|
);
|
||||||
|
return account.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): IProgramStateClass {
|
||||||
|
return {
|
||||||
|
publicKey: this.account.publicKey,
|
||||||
|
authorityPublicKey: this.authorityPublicKey,
|
||||||
|
tokenMintPublicKey: this.tokenMintPublicKey,
|
||||||
|
tokenVaultPublicKey: this.tokenVaultPublicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(this.toJSON(), pubKeyConverter, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrint(): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Program State", this.publicKey) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString +=
|
||||||
|
chalkString("programId", this.account.program.programId) + "\r\n";
|
||||||
|
outputString += chalkString("authority", this.authorityPublicKey) + "\r\n";
|
||||||
|
outputString += chalkString("tokenMint", this.tokenMintPublicKey) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("tokenVault", this.tokenVaultPublicKey) + "\r\n";
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
|
||||||
|
prettyPrintTokenMint(): string {
|
||||||
|
let outputString = "";
|
||||||
|
|
||||||
|
outputString += chalk.underline(
|
||||||
|
chalkString("## Token Mint", this.token.publicKey) + "\r\n"
|
||||||
|
);
|
||||||
|
outputString += chalkString("decimals", this.mintInfo.decimals) + "\r\n";
|
||||||
|
outputString +=
|
||||||
|
chalkString("supply", this.mintInfo.supply.toString()) + "\r\n";
|
||||||
|
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async print(program: anchor.Program): Promise<string> {
|
||||||
|
const accountClass = await ProgramStateClass.build(program);
|
||||||
|
return accountClass.prettyPrint() + accountClass.prettyPrintTokenMint();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
|
export interface ProgramStateData {
|
||||||
|
authority: PublicKey;
|
||||||
|
tokenMint: PublicKey;
|
||||||
|
tokenVault: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProgramStateClass {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
authorityPublicKey: PublicKey;
|
||||||
|
tokenMintPublicKey: PublicKey;
|
||||||
|
tokenVaultPublicKey: PublicKey;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./types";
|
|
@ -0,0 +1,83 @@
|
||||||
|
/* eslint-disable no-use-before-define */
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
CrankAccount,
|
||||||
|
JobAccount,
|
||||||
|
LeaseAccount,
|
||||||
|
OracleAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
PermissionAccount,
|
||||||
|
ProgramStateAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
|
||||||
|
export const DEFAULT_KEYPAIR = Keypair.fromSeed(new Uint8Array(32).fill(1));
|
||||||
|
export const DEFAULT_PUBKEY = new PublicKey("11111111111111111111111111111111");
|
||||||
|
|
||||||
|
/** An existing on-chain account to load */
|
||||||
|
export interface fromPublicKey {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SWITCHBOARD_ACCOUNT_TYPES = [
|
||||||
|
"JobAccountData",
|
||||||
|
"AggregatorAccountData",
|
||||||
|
"OracleAccountData",
|
||||||
|
"OracleQueueAccountData",
|
||||||
|
"PermissionAccountData",
|
||||||
|
"LeaseAccountData",
|
||||||
|
"ProgramStateAccountData",
|
||||||
|
"VrfAccountData",
|
||||||
|
"SbState",
|
||||||
|
"BUFFERxx",
|
||||||
|
"CrankAccountData",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type SwitchboardAccountType = typeof SWITCHBOARD_ACCOUNT_TYPES[number];
|
||||||
|
|
||||||
|
export const SWITCHBOARD_DISCRIMINATOR_MAP = new Map<
|
||||||
|
SwitchboardAccountType,
|
||||||
|
Buffer
|
||||||
|
>(
|
||||||
|
SWITCHBOARD_ACCOUNT_TYPES.map((accountType) => [
|
||||||
|
accountType,
|
||||||
|
anchor.BorshAccountsCoder.accountDiscriminator(accountType),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
// export type SwitchboardAccountType =
|
||||||
|
// | "JobAccount"
|
||||||
|
// | "AggregatorAccount"
|
||||||
|
// | "OracleAccount"
|
||||||
|
// | "OracleQueueAccount"
|
||||||
|
// | "PermissionAccount"
|
||||||
|
// | "LeaseAccount"
|
||||||
|
// | "ProgramStateAccount"
|
||||||
|
// | "CrankAccount";
|
||||||
|
|
||||||
|
export type SwitchboardAccount =
|
||||||
|
| JobAccount
|
||||||
|
| AggregatorAccount
|
||||||
|
| OracleAccount
|
||||||
|
| ProgramStateAccount
|
||||||
|
| CrankAccount
|
||||||
|
| PermissionAccount
|
||||||
|
| LeaseAccount
|
||||||
|
| OracleQueueAccount;
|
||||||
|
|
||||||
|
export interface fromSwitchboardAccount {
|
||||||
|
account: SwitchboardAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copy an existing account type */
|
||||||
|
export interface copyAccount {
|
||||||
|
sourcePublicKey: PublicKey;
|
||||||
|
existingKeypair?: Keypair;
|
||||||
|
authorityKeypair?: Keypair;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load a JSON definition from a file path */
|
||||||
|
export interface jsonPath {
|
||||||
|
jsonPath: string;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./utils";
|
|
@ -0,0 +1,424 @@
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { ACCOUNT_DISCRIMINATOR_SIZE } from "@project-serum/anchor/dist/cjs/coder";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
CrankAccount,
|
||||||
|
JobAccount,
|
||||||
|
LeaseAccount,
|
||||||
|
OracleAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
PermissionAccount,
|
||||||
|
ProgramStateAccount,
|
||||||
|
SwitchboardDecimal,
|
||||||
|
SwitchboardPermission,
|
||||||
|
SwitchboardPermissionValue,
|
||||||
|
VrfAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import Big from "big.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import {
|
||||||
|
AggregatorClass,
|
||||||
|
CrankClass,
|
||||||
|
JobClass,
|
||||||
|
LeaseClass,
|
||||||
|
OracleClass,
|
||||||
|
OracleQueueClass,
|
||||||
|
PermissionClass,
|
||||||
|
ProgramStateClass,
|
||||||
|
} from "..";
|
||||||
|
import { CommandContext } from "../../types";
|
||||||
|
import { loadKeypair } from "../../utils";
|
||||||
|
import {
|
||||||
|
SwitchboardAccountType,
|
||||||
|
SWITCHBOARD_DISCRIMINATOR_MAP,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export const getArrayOfSizeN = (number_: number): number[] => {
|
||||||
|
return Array.from({ length: number_ }, (_, index) => index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// list of keys that will not be included in a json output file
|
||||||
|
const IGNORE_JSON_OUTPUT_KEYS = new Set<string>([
|
||||||
|
"account",
|
||||||
|
"isActive",
|
||||||
|
"logger",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const toPermissionString = (
|
||||||
|
permission: SwitchboardPermissionValue
|
||||||
|
): string => {
|
||||||
|
switch (permission) {
|
||||||
|
case SwitchboardPermissionValue.PERMIT_ORACLE_HEARTBEAT:
|
||||||
|
return "PERMIT_ORACLE_HEARTBEAT";
|
||||||
|
case SwitchboardPermissionValue.PERMIT_ORACLE_QUEUE_USAGE:
|
||||||
|
return "PERMIT_ORACLE_QUEUE_USAGE";
|
||||||
|
default:
|
||||||
|
return "NONE";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum VrfStatus {
|
||||||
|
STATUS_NONE = "statusNone",
|
||||||
|
STATUS_REQUESTING = "statusRequesting",
|
||||||
|
STATUS_VERIFYING = "statusVerifying",
|
||||||
|
STATUS_VERIFIED = "statusVerifying",
|
||||||
|
STATUS_CALLBACK_SUCCESS = "statusCallbackSuccess",
|
||||||
|
STATUS_VERIFY_FAILURE = "statusVerifyFailure",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toVrfStatus1 = (status: object): string => {
|
||||||
|
if ("statusNone" in status) {
|
||||||
|
return "StatusNone";
|
||||||
|
}
|
||||||
|
if ("statusRequesting" in status) {
|
||||||
|
return "StatusRequesting";
|
||||||
|
}
|
||||||
|
if ("statusVerifying" in status) {
|
||||||
|
return "StatusVerifying";
|
||||||
|
}
|
||||||
|
if ("statusVerified" in status) {
|
||||||
|
return "StatusVerified";
|
||||||
|
}
|
||||||
|
if ("statusCallbackSuccess" in status) {
|
||||||
|
return "StatusCallbackSuccess";
|
||||||
|
}
|
||||||
|
if ("statusVerifyFailure" in status) {
|
||||||
|
return "StatusVerifyFailure";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toVrfStatus = (status: object): VrfStatus => {
|
||||||
|
if ("statusNone" in status) {
|
||||||
|
return VrfStatus.STATUS_NONE;
|
||||||
|
}
|
||||||
|
if ("statusRequesting" in status) {
|
||||||
|
return VrfStatus.STATUS_REQUESTING;
|
||||||
|
}
|
||||||
|
if ("statusVerifying" in status) {
|
||||||
|
return VrfStatus.STATUS_VERIFYING;
|
||||||
|
}
|
||||||
|
if ("statusVerified" in status) {
|
||||||
|
return VrfStatus.STATUS_VERIFIED;
|
||||||
|
}
|
||||||
|
if ("statusCallbackSuccess" in status) {
|
||||||
|
return VrfStatus.STATUS_CALLBACK_SUCCESS;
|
||||||
|
}
|
||||||
|
if ("statusVerifyFailure" in status) {
|
||||||
|
return VrfStatus.STATUS_VERIFY_FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toPermission = (
|
||||||
|
permissionString: string
|
||||||
|
): SwitchboardPermission => {
|
||||||
|
switch (permissionString) {
|
||||||
|
case "PERMIT_ORACLE_HEARTBEAT":
|
||||||
|
return SwitchboardPermission.PERMIT_ORACLE_HEARTBEAT;
|
||||||
|
case "PERMIT_ORACLE_QUEUE_USAGE":
|
||||||
|
return SwitchboardPermission.PERMIT_ORACLE_QUEUE_USAGE;
|
||||||
|
default:
|
||||||
|
return SwitchboardPermission[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// JSON.stringify: Object => String
|
||||||
|
export const pubKeyConverter = (key: any, value: any): any => {
|
||||||
|
if (value instanceof PublicKey || key.toLowerCase().endsWith("publickey")) {
|
||||||
|
return value.toString() ?? "";
|
||||||
|
}
|
||||||
|
if (value instanceof Uint8Array) {
|
||||||
|
return `[${value.toString()}]`;
|
||||||
|
}
|
||||||
|
if (value instanceof anchor.BN) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Big) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof SwitchboardDecimal) {
|
||||||
|
return new Big(value.mantissa.toString())
|
||||||
|
.div(new Big(10).pow(value.scale))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
if (IGNORE_JSON_OUTPUT_KEYS.has(key)) return undefined;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// JSON.parse: String => Object
|
||||||
|
export const pubKeyReviver = (key, value): any => {
|
||||||
|
if (key.toLowerCase().endsWith("publickey")) {
|
||||||
|
return new PublicKey(value);
|
||||||
|
}
|
||||||
|
if (key.toLowerCase().endsWith("secretkey")) {
|
||||||
|
return new Uint8Array(JSON.parse(value));
|
||||||
|
}
|
||||||
|
if (key.toLowerCase().endsWith("keypair")) {
|
||||||
|
return loadKeypair(value);
|
||||||
|
}
|
||||||
|
if (key.toLowerCase().startsWith("variancethreshold")) {
|
||||||
|
return new SwitchboardDecimal(new anchor.BN(value.mantissa), value.scale);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chalkString = (
|
||||||
|
label: string,
|
||||||
|
value: string | number | boolean | PublicKey | Big | anchor.BN,
|
||||||
|
padding = 16
|
||||||
|
): string => {
|
||||||
|
return `${chalk.blue(label.padEnd(padding, " "))}${chalk.yellow(
|
||||||
|
value ? value.toString() : "undefined"
|
||||||
|
)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-disable no-control-regex */
|
||||||
|
export const buffer2string = (buf: Buffer | string | ArrayBuffer): string => {
|
||||||
|
return Buffer.from(buf as any)
|
||||||
|
.toString("utf8")
|
||||||
|
.replace(/\u0000/g, ""); // removes padding from onchain fixed sized buffers
|
||||||
|
};
|
||||||
|
|
||||||
|
const padTime = (number_: number): string => {
|
||||||
|
return number_.toString().padStart(2, "0");
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toDateString(d: Date | undefined): string {
|
||||||
|
if (d)
|
||||||
|
return `${d.getFullYear()}-${padTime(d.getMonth() + 1)}-${padTime(
|
||||||
|
d.getDate()
|
||||||
|
)} L`;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anchorBNtoDateString(ts: anchor.BN): string {
|
||||||
|
if (!ts.toNumber()) return "N/A";
|
||||||
|
return toDateString(new Date(ts.toNumber() * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDateTimeString(d: Date | undefined): string {
|
||||||
|
if (d)
|
||||||
|
return `${d.getFullYear()}-${padTime(d.getMonth() + 1)}-${padTime(
|
||||||
|
d.getDate()
|
||||||
|
)} ${padTime(d.getHours())}:${padTime(d.getMinutes())}:${padTime(
|
||||||
|
d.getSeconds()
|
||||||
|
)} L`;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anchorBNtoDateTimeString(ts: anchor.BN): string {
|
||||||
|
if (!ts.toNumber()) return "N/A";
|
||||||
|
return toDateTimeString(new Date(ts.toNumber() * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isJobAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<JobAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new JobAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAggregatorAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<AggregatorAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new AggregatorAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isOracleAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<OracleAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new OracleAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCrankAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<CrankAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new CrankAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isOracleQueueAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<OracleQueueAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new OracleQueueAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPermissionAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<PermissionAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new PermissionAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLeaseAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<LeaseAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new LeaseAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isProgramStateAccount = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<ProgramStateAccount | undefined> => {
|
||||||
|
try {
|
||||||
|
const account = new ProgramStateAccount({ program, publicKey });
|
||||||
|
await account.loadData();
|
||||||
|
return account;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// should also check if pubkey is a token account
|
||||||
|
export const findAccountType = async (
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<SwitchboardAccountType> => {
|
||||||
|
const account = await program.provider.connection.getAccountInfo(publicKey);
|
||||||
|
const accountDiscriminator = account.data.slice(
|
||||||
|
0,
|
||||||
|
ACCOUNT_DISCRIMINATOR_SIZE
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [name, discriminator] of SWITCHBOARD_DISCRIMINATOR_MAP.entries()) {
|
||||||
|
if (Buffer.compare(accountDiscriminator, discriminator) === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`no switchboard account found for ${publicKey}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildClassFromKey = async (
|
||||||
|
context: CommandContext,
|
||||||
|
program: anchor.Program,
|
||||||
|
publicKey: PublicKey
|
||||||
|
): Promise<
|
||||||
|
| JobClass
|
||||||
|
| AggregatorClass
|
||||||
|
| OracleClass
|
||||||
|
| PermissionClass
|
||||||
|
| LeaseClass
|
||||||
|
| OracleQueueClass
|
||||||
|
| CrankClass
|
||||||
|
| ProgramStateClass
|
||||||
|
| VrfAccount
|
||||||
|
> => {
|
||||||
|
const accountType = await findAccountType(program, publicKey);
|
||||||
|
switch (accountType) {
|
||||||
|
case "JobAccountData": {
|
||||||
|
const job = await JobClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new JobAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(job.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "AggregatorAccountData": {
|
||||||
|
const aggregator = await AggregatorClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new AggregatorAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(aggregator.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "OracleAccountData": {
|
||||||
|
const oracle = await OracleClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new OracleAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(oracle.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "PermissionAccountData": {
|
||||||
|
const permission = await PermissionClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new PermissionAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(permission.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "LeaseAccountData": {
|
||||||
|
const lease = await LeaseClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new LeaseAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(lease.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "OracleQueueAccountData": {
|
||||||
|
const queue = await OracleQueueClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new OracleQueueAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(queue.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "CrankAccountData": {
|
||||||
|
const crank = await CrankClass.fromAccount(
|
||||||
|
context,
|
||||||
|
new CrankAccount({ program, publicKey })
|
||||||
|
);
|
||||||
|
context.logger.log(crank.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ProgramStateAccountData": {
|
||||||
|
const state = await ProgramStateClass.build(program);
|
||||||
|
context.logger.log(state.prettyPrint());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// case "VrfAccountData": {
|
||||||
|
// const state = new VrfAccount
|
||||||
|
// context.logger.log(state.prettyPrint());
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
throw new Error(`no switchboard account found for ${publicKey}`);
|
||||||
|
};
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount, JobAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import {
|
||||||
|
AggregatorClass,
|
||||||
|
fromJobJSON,
|
||||||
|
JobClass,
|
||||||
|
pubKeyConverter,
|
||||||
|
pubKeyReviver,
|
||||||
|
} from "../../../accounts";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { OutputFileExistsNoForce } from "../../../types";
|
||||||
|
import { CHECK_ICON, loadKeypair } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorAddJob extends BaseCommand {
|
||||||
|
aggregatorAuthority?: Keypair | undefined = undefined;
|
||||||
|
|
||||||
|
aggregatorAccount: AggregatorAccount;
|
||||||
|
|
||||||
|
jobDefinition?: fromJobJSON = undefined;
|
||||||
|
|
||||||
|
jobAccount?: JobAccount;
|
||||||
|
|
||||||
|
outputFile = "";
|
||||||
|
|
||||||
|
static description = "add a job account to an aggregator";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
force: flags.boolean({
|
||||||
|
description: "overwrite outputFile if existing",
|
||||||
|
}),
|
||||||
|
outputFile: flags.string({
|
||||||
|
char: "f",
|
||||||
|
description: "output file to save aggregator definition to",
|
||||||
|
}),
|
||||||
|
jobDefinition: flags.string({
|
||||||
|
description: "filesystem path of job json definition file",
|
||||||
|
exclusive: ["jobKey"],
|
||||||
|
}),
|
||||||
|
jobKey: flags.string({
|
||||||
|
description:
|
||||||
|
"public key of an existing job account to add to an aggregator",
|
||||||
|
exclusive: ["jobDefinition"],
|
||||||
|
}),
|
||||||
|
aggregatorAuthority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = ["$ sbv2 aggregator:add:job"];
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await super.init();
|
||||||
|
const { args, flags } = this.parse(AggregatorAddJob);
|
||||||
|
|
||||||
|
if (flags.aggregatorAuthority) {
|
||||||
|
this.aggregatorAuthority = await loadKeypair(flags.aggregatorAuthority);
|
||||||
|
this.context.logger.debug(
|
||||||
|
`using aggregator authority ${this.aggregatorAuthority.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flags.jobDefinition) {
|
||||||
|
this.jobDefinition = JSON.parse(flags.jobDefinition, pubKeyReviver);
|
||||||
|
}
|
||||||
|
if (flags.jobKey) {
|
||||||
|
this.jobAccount = new JobAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: new PublicKey(flags.jobKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.jobDefinition && !this.jobAccount) {
|
||||||
|
throw new Error("need to provide --jobDefinition or --jobKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.outputFile) {
|
||||||
|
if (fs.existsSync(flags.outputFile) && !flags.force) {
|
||||||
|
throw new OutputFileExistsNoForce(flags.outputFile);
|
||||||
|
}
|
||||||
|
this.outputFile = flags.outputFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const job = this.jobAccount
|
||||||
|
? await JobClass.fromAccount(this.context, this.jobAccount)
|
||||||
|
: await JobClass.build(this.context, this.program, this.jobDefinition);
|
||||||
|
|
||||||
|
const txn = await this.aggregatorAccount.addJob(
|
||||||
|
job.account,
|
||||||
|
this.aggregatorAuthority
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregator = await AggregatorClass.fromAccount(
|
||||||
|
this.context,
|
||||||
|
this.aggregatorAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Job succesfully added to aggregator account\r\n`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.outputFile) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
this.outputFile,
|
||||||
|
JSON.stringify(aggregator, pubKeyConverter, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to add job to aggregator account");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,432 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import * as spl from "@solana/spl-token";
|
||||||
|
import {
|
||||||
|
AccountInfo,
|
||||||
|
Keypair,
|
||||||
|
PublicKey,
|
||||||
|
SystemProgram,
|
||||||
|
TransactionInstruction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
prettyPrintAggregator,
|
||||||
|
promiseWithTimeout,
|
||||||
|
} from "@switchboard-xyz/sbv2-utils";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
CrankAccount,
|
||||||
|
JobAccount,
|
||||||
|
LeaseAccount,
|
||||||
|
OracleJob,
|
||||||
|
OracleQueueAccount,
|
||||||
|
PermissionAccount,
|
||||||
|
ProgramStateAccount,
|
||||||
|
programWallet,
|
||||||
|
SwitchboardDecimal,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import Big from "big.js";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { verifyProgramHasPayer } from "../../../utils";
|
||||||
|
import { packAndSend } from "../../../utils/transaction";
|
||||||
|
|
||||||
|
export default class AggregatorCreateCopy extends BaseCommand {
|
||||||
|
static description = "copy an aggregator account to a new oracle queue";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
force: flags.boolean({ description: "skip job confirmation" }),
|
||||||
|
outputFile: flags.string({
|
||||||
|
char: "f",
|
||||||
|
description: "output file to save aggregator definition to",
|
||||||
|
}),
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that will be the aggregator authority",
|
||||||
|
}),
|
||||||
|
minOracles: flags.integer({
|
||||||
|
description: "override source aggregator's minOracleResults",
|
||||||
|
}),
|
||||||
|
batchSize: flags.integer({
|
||||||
|
description: "override source aggregator's oracleRequestBatchSize",
|
||||||
|
}),
|
||||||
|
minJobs: flags.integer({
|
||||||
|
description: "override source aggregator's minJobResults",
|
||||||
|
}),
|
||||||
|
minUpdateDelay: flags.integer({
|
||||||
|
description: "override source aggregator's minUpdateDelaySeconds",
|
||||||
|
}),
|
||||||
|
forceReportPeriod: flags.integer({
|
||||||
|
description: "override source aggregator's forceReportPeriod",
|
||||||
|
}),
|
||||||
|
varianceThreshold: flags.string({
|
||||||
|
description: "override source aggregator's varianceThreshold",
|
||||||
|
}),
|
||||||
|
queueKey: flags.string({
|
||||||
|
description: "public key of the queue to create aggregator for",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
crankKey: flags.string({
|
||||||
|
description: "public key of the crank to push aggregator to",
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorSource",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account to copy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:create:copy 8SXvChNYFhRq4EZuZvnhjrB3jJRQCv4k3P4W6hesH3Ee AY3vpUu6v49shWajeFjHjgikYfaBWNJgax8zoEouUDTs --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
const { args, flags } = this.parse(AggregatorCreateCopy);
|
||||||
|
|
||||||
|
const payerKeypair = programWallet(this.program);
|
||||||
|
|
||||||
|
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
|
||||||
|
this.program
|
||||||
|
);
|
||||||
|
const programState = await programStateAccount.loadData();
|
||||||
|
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: new PublicKey(flags.queueKey),
|
||||||
|
});
|
||||||
|
const queue = await queueAccount.loadData();
|
||||||
|
const tokenMint = await queueAccount.loadMint();
|
||||||
|
const tokenWallet = (
|
||||||
|
await tokenMint.getOrCreateAssociatedAccountInfo(payerKeypair.publicKey)
|
||||||
|
).address;
|
||||||
|
|
||||||
|
const sourceAggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorSource,
|
||||||
|
});
|
||||||
|
const sourceAggregator = await sourceAggregatorAccount.loadData();
|
||||||
|
const sourceJobPubkeys: PublicKey[] = sourceAggregator.jobPubkeysData.slice(
|
||||||
|
0,
|
||||||
|
sourceAggregator.jobPubkeysSize
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceJobAccounts = sourceJobPubkeys.map((publicKey) => {
|
||||||
|
return new JobAccount({ program: this.program, publicKey: publicKey });
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceJobs = await Promise.all(
|
||||||
|
sourceJobAccounts.map(async (jobAccount) => {
|
||||||
|
const data = await jobAccount.loadData();
|
||||||
|
const job = OracleJob.decodeDelimited(data.data);
|
||||||
|
return { job, data };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const createAccountInstructions: (
|
||||||
|
| TransactionInstruction
|
||||||
|
| TransactionInstruction[]
|
||||||
|
)[] = [];
|
||||||
|
const createAccountSigners: Keypair[] = [payerKeypair];
|
||||||
|
|
||||||
|
const jobAccounts = await Promise.all(
|
||||||
|
sourceJobs.map(async ({ job, data }) => {
|
||||||
|
const jobKeypair = Keypair.generate();
|
||||||
|
createAccountSigners.push(jobKeypair);
|
||||||
|
|
||||||
|
const jobData = Buffer.from(
|
||||||
|
OracleJob.encodeDelimited(
|
||||||
|
OracleJob.create({
|
||||||
|
tasks: job.tasks,
|
||||||
|
})
|
||||||
|
).finish()
|
||||||
|
);
|
||||||
|
const size =
|
||||||
|
280 + jobData.length + (data.variables?.join("")?.length ?? 0);
|
||||||
|
|
||||||
|
createAccountInstructions.push([
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: payerKeypair.publicKey,
|
||||||
|
newAccountPubkey: jobKeypair.publicKey,
|
||||||
|
space: size,
|
||||||
|
lamports:
|
||||||
|
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||||
|
size
|
||||||
|
),
|
||||||
|
programId: this.program.programId,
|
||||||
|
}),
|
||||||
|
await this.program.methods
|
||||||
|
.jobInit({
|
||||||
|
name: Buffer.from(data.name),
|
||||||
|
data: jobData,
|
||||||
|
variables:
|
||||||
|
data.variables?.map((item) => Buffer.from("")) ??
|
||||||
|
new Array<Buffer>(),
|
||||||
|
authorWallet: payerKeypair.publicKey,
|
||||||
|
stateBump,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
job: jobKeypair.publicKey,
|
||||||
|
authorWallet: tokenWallet,
|
||||||
|
authority: payerKeypair.publicKey,
|
||||||
|
programState: programStateAccount.publicKey,
|
||||||
|
})
|
||||||
|
// .signers([jobKeypair])
|
||||||
|
.instruction(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new JobAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: jobKeypair.publicKey,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorKeypair = Keypair.generate();
|
||||||
|
this.logger.debug(`Aggregator: ${aggregatorKeypair.publicKey}`);
|
||||||
|
createAccountSigners.push(aggregatorKeypair);
|
||||||
|
const aggregatorSize = this.program.account.aggregatorAccountData.size;
|
||||||
|
const permissionAccountSize =
|
||||||
|
this.program.account.permissionAccountData.size;
|
||||||
|
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
|
||||||
|
this.program,
|
||||||
|
queue.authority,
|
||||||
|
queueAccount.publicKey,
|
||||||
|
aggregatorKeypair.publicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: aggregatorKeypair.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create lease and push to crank
|
||||||
|
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
|
||||||
|
this.program,
|
||||||
|
queueAccount,
|
||||||
|
aggregatorAccount
|
||||||
|
);
|
||||||
|
const leaseEscrow = await spl.Token.getAssociatedTokenAddress(
|
||||||
|
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
spl.TOKEN_PROGRAM_ID,
|
||||||
|
tokenMint.publicKey,
|
||||||
|
leaseAccount.publicKey,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const jobPubkeys: Array<PublicKey> = [];
|
||||||
|
const jobWallets: Array<PublicKey> = [];
|
||||||
|
const walletBumps: Array<number> = [];
|
||||||
|
for (let idx in jobAccounts) {
|
||||||
|
const [jobWallet, bump] = anchor.utils.publicKey.findProgramAddressSync(
|
||||||
|
[
|
||||||
|
payerKeypair.publicKey.toBuffer(),
|
||||||
|
spl.TOKEN_PROGRAM_ID.toBuffer(),
|
||||||
|
tokenMint.publicKey.toBuffer(),
|
||||||
|
],
|
||||||
|
spl.ASSOCIATED_TOKEN_PROGRAM_ID
|
||||||
|
);
|
||||||
|
jobPubkeys.push(jobAccounts[idx].publicKey);
|
||||||
|
jobWallets.push(jobWallet);
|
||||||
|
walletBumps.push(bump);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccountInstructions.push(
|
||||||
|
[
|
||||||
|
// allocate aggregator space
|
||||||
|
SystemProgram.createAccount({
|
||||||
|
fromPubkey: payerKeypair.publicKey,
|
||||||
|
newAccountPubkey: aggregatorKeypair.publicKey,
|
||||||
|
space: aggregatorSize,
|
||||||
|
lamports:
|
||||||
|
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||||
|
aggregatorSize
|
||||||
|
),
|
||||||
|
programId: this.program.programId,
|
||||||
|
}),
|
||||||
|
// create aggregator
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorInit({
|
||||||
|
name: sourceAggregator.name,
|
||||||
|
metadata: sourceAggregator.metadata,
|
||||||
|
batchSize:
|
||||||
|
flags.batchSize ?? sourceAggregator.oracleRequestBatchSize,
|
||||||
|
minOracleResults:
|
||||||
|
flags.minOracles ?? sourceAggregator.minOracleResults,
|
||||||
|
minJobResults: flags.minJobs ?? sourceAggregator.minJobResults,
|
||||||
|
minUpdateDelaySeconds:
|
||||||
|
flags.minUpdateDelay ?? sourceAggregator.minUpdateDelaySeconds,
|
||||||
|
varianceThreshold: flags.varianceThreshold
|
||||||
|
? SwitchboardDecimal.fromBig(new Big(flags.varianceThreshold))
|
||||||
|
: sourceAggregator.varianceThreshold,
|
||||||
|
forceReportPeriod:
|
||||||
|
flags.forceReportPeriod ?? sourceAggregator.forceReportPeriod,
|
||||||
|
stateBump,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorKeypair.publicKey,
|
||||||
|
authority: payerKeypair.publicKey,
|
||||||
|
queue: queueAccount.publicKey,
|
||||||
|
authorWallet: tokenWallet,
|
||||||
|
programState: programStateAccount.publicKey,
|
||||||
|
})
|
||||||
|
.instruction(),
|
||||||
|
// create permissions
|
||||||
|
await this.program.methods
|
||||||
|
.permissionInit({})
|
||||||
|
.accounts({
|
||||||
|
permission: permissionAccount.publicKey,
|
||||||
|
authority: queue.authority,
|
||||||
|
granter: queueAccount.publicKey,
|
||||||
|
grantee: aggregatorKeypair.publicKey,
|
||||||
|
payer: payerKeypair.publicKey,
|
||||||
|
systemProgram: SystemProgram.programId,
|
||||||
|
})
|
||||||
|
.instruction(),
|
||||||
|
payerKeypair.publicKey.equals(queue.authority)
|
||||||
|
? await this.program.methods
|
||||||
|
.permissionSet({
|
||||||
|
permission: { permitOracleQueueUsage: null },
|
||||||
|
enable: true,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
permission: permissionAccount.publicKey,
|
||||||
|
authority: queue.authority,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
: undefined,
|
||||||
|
spl.Token.createAssociatedTokenAccountInstruction(
|
||||||
|
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
spl.TOKEN_PROGRAM_ID,
|
||||||
|
tokenMint.publicKey,
|
||||||
|
leaseEscrow,
|
||||||
|
leaseAccount.publicKey,
|
||||||
|
payerKeypair.publicKey
|
||||||
|
),
|
||||||
|
await this.program.methods
|
||||||
|
.leaseInit({
|
||||||
|
loadAmount: new anchor.BN(0),
|
||||||
|
stateBump,
|
||||||
|
leaseBump,
|
||||||
|
withdrawAuthority: payerKeypair.publicKey,
|
||||||
|
walletBumps: Buffer.from([]),
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
programState: programStateAccount.publicKey,
|
||||||
|
lease: leaseAccount.publicKey,
|
||||||
|
queue: queueAccount.publicKey,
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
systemProgram: SystemProgram.programId,
|
||||||
|
funder: tokenWallet,
|
||||||
|
payer: payerKeypair.publicKey,
|
||||||
|
tokenProgram: spl.TOKEN_PROGRAM_ID,
|
||||||
|
escrow: leaseEscrow,
|
||||||
|
owner: payerKeypair.publicKey,
|
||||||
|
mint: tokenMint.publicKey,
|
||||||
|
})
|
||||||
|
// .remainingAccounts(
|
||||||
|
// jobPubkeys.concat(jobWallets).map((pubkey: PublicKey) => {
|
||||||
|
// return { isSigner: false, isWritable: true, pubkey };
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
.instruction(),
|
||||||
|
flags.crankKey
|
||||||
|
? await this.program.methods
|
||||||
|
.crankPush({
|
||||||
|
stateBump,
|
||||||
|
permissionBump,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
crank: new PublicKey(flags.crankKey),
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
oracleQueue: queueAccount.publicKey,
|
||||||
|
queueAuthority: queue.authority,
|
||||||
|
permission: permissionAccount.publicKey,
|
||||||
|
lease: leaseAccount.publicKey,
|
||||||
|
escrow: leaseEscrow,
|
||||||
|
programState: programStateAccount.publicKey,
|
||||||
|
dataBuffer: (
|
||||||
|
await new CrankAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: new PublicKey(flags.crankKey),
|
||||||
|
}).loadData()
|
||||||
|
).dataBuffer,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
: undefined,
|
||||||
|
].filter((item) => item)
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalInstructions: (
|
||||||
|
| TransactionInstruction
|
||||||
|
| TransactionInstruction[]
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
|
finalInstructions.push(
|
||||||
|
...(await Promise.all(
|
||||||
|
jobAccounts.map(async (jobAccount) => {
|
||||||
|
return this.program.methods
|
||||||
|
.aggregatorAddJob({
|
||||||
|
weight: 1,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorKeypair.publicKey,
|
||||||
|
authority: payerKeypair.publicKey,
|
||||||
|
job: jobAccount.publicKey,
|
||||||
|
})
|
||||||
|
.instruction();
|
||||||
|
})
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
const createAccountSignatures = packAndSend(
|
||||||
|
this.program,
|
||||||
|
createAccountInstructions,
|
||||||
|
finalInstructions,
|
||||||
|
createAccountSigners,
|
||||||
|
payerKeypair.publicKey
|
||||||
|
);
|
||||||
|
|
||||||
|
let aggInitWs: number;
|
||||||
|
const aggInitPromise = new Promise((resolve: (result: boolean) => void) => {
|
||||||
|
aggInitWs = this.program.provider.connection.onAccountChange(
|
||||||
|
aggregatorAccount.publicKey,
|
||||||
|
(accountInfo: AccountInfo<Buffer>, slot) => {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const awaitResult = await promiseWithTimeout(
|
||||||
|
22_000,
|
||||||
|
aggInitPromise
|
||||||
|
).finally(() => {
|
||||||
|
try {
|
||||||
|
this.program.provider.connection.removeAccountChangeListener(aggInitWs);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(aggregatorAccount.publicKey.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
await prettyPrintAggregator(
|
||||||
|
aggregatorAccount,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "Failed to copy aggregator account to new queue");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { prettyPrintAggregator } from "@switchboard-xyz/sbv2-utils";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
JobAccount,
|
||||||
|
OracleJob,
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import {
|
||||||
|
fromAggregatorJSON,
|
||||||
|
fromJobJSON,
|
||||||
|
pubKeyConverter,
|
||||||
|
pubKeyReviver,
|
||||||
|
} from "../../../accounts";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, loadKeypair, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class JsonCreateAggregator extends BaseCommand {
|
||||||
|
static description = "create an aggregator from a json file";
|
||||||
|
|
||||||
|
static aliases = ["json:create:aggregator"];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
force: flags.boolean({
|
||||||
|
description: "overwrite output file",
|
||||||
|
}),
|
||||||
|
outputFile: flags.string({
|
||||||
|
description: "output aggregator definition to a json file",
|
||||||
|
char: "f",
|
||||||
|
}),
|
||||||
|
queueKey: flags.string({
|
||||||
|
description: "public key of the oracle queue to create aggregator for",
|
||||||
|
char: "q",
|
||||||
|
}),
|
||||||
|
authority: flags.string({
|
||||||
|
description:
|
||||||
|
"alternate keypair that will be the authority for the aggregator",
|
||||||
|
char: "a",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "definitionFile",
|
||||||
|
required: true,
|
||||||
|
description: "filesystem path of queue definition json file",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:create:json examples/aggregator.json --keypair ../payer-keypair.json --queueKey GhYg3R1V6DmJbwuc57qZeoYG6gUuvCotUF1zU3WCj98U --outputFile aggregator.schema.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(JsonCreateAggregator);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const payerKeypair = programWallet(this.program);
|
||||||
|
|
||||||
|
const definitionFile = args.definitionFile.startsWith("/")
|
||||||
|
? args.definitionFile
|
||||||
|
: path.join(process.cwd(), args.definitionFile);
|
||||||
|
if (!fs.existsSync(definitionFile)) {
|
||||||
|
throw new Error("input file does not exist");
|
||||||
|
}
|
||||||
|
let aggregatorDefinition: fromAggregatorJSON = JSON.parse(
|
||||||
|
fs.readFileSync(definitionFile, "utf-8"),
|
||||||
|
pubKeyReviver
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flags.outputFile) {
|
||||||
|
if (fs.existsSync(flags.outputFile) && !flags.force) {
|
||||||
|
throw new Error(
|
||||||
|
"output file exists. Run the command with '--force' to overwrite it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let authority = programWallet(this.program);
|
||||||
|
if (flags.authority) {
|
||||||
|
authority = await loadKeypair(flags.authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!aggregatorDefinition.queuePublicKey && !flags.queueKey) {
|
||||||
|
throw new Error("you must provide a --queueKey to create aggregator for");
|
||||||
|
}
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: aggregatorDefinition.queuePublicKey
|
||||||
|
? new PublicKey(aggregatorDefinition.queuePublicKey)
|
||||||
|
: new PublicKey(flags.queueKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorWallet =
|
||||||
|
aggregatorDefinition.authorWalletPublicKey ?? payerKeypair.publicKey;
|
||||||
|
const authorityPubkey = authority.publicKey ?? payerKeypair.publicKey;
|
||||||
|
const batchSize = aggregatorDefinition.oracleRequestBatchSize ?? 3;
|
||||||
|
const expiration = aggregatorDefinition.expiration
|
||||||
|
? new anchor.BN(aggregatorDefinition.expiration)
|
||||||
|
: new anchor.BN(0);
|
||||||
|
const forceReportPeriod = aggregatorDefinition.forceReportPeriod
|
||||||
|
? new anchor.BN(aggregatorDefinition.forceReportPeriod)
|
||||||
|
: new anchor.BN(0);
|
||||||
|
const metadata = aggregatorDefinition.metadata
|
||||||
|
? Buffer.from(aggregatorDefinition.metadata)
|
||||||
|
: Buffer.from("");
|
||||||
|
const minRequiredJobResults =
|
||||||
|
aggregatorDefinition.minRequiredJobResults ?? 1;
|
||||||
|
const minRequiredOracleResults =
|
||||||
|
aggregatorDefinition.minRequiredOracleResults ?? 2;
|
||||||
|
const minUpdateDelaySeconds =
|
||||||
|
aggregatorDefinition.minUpdateDelaySeconds ?? 30;
|
||||||
|
const name = aggregatorDefinition.name
|
||||||
|
? Buffer.from(aggregatorDefinition.name)
|
||||||
|
: Buffer.from("");
|
||||||
|
const startAfter = aggregatorDefinition.startAfter ?? 0;
|
||||||
|
const varianceThreshold = aggregatorDefinition.varianceThreshold ?? 0;
|
||||||
|
|
||||||
|
const aggregatorAccount = await AggregatorAccount.create(this.program, {
|
||||||
|
authorWallet,
|
||||||
|
authority: authorityPubkey,
|
||||||
|
batchSize,
|
||||||
|
expiration,
|
||||||
|
forceReportPeriod,
|
||||||
|
metadata,
|
||||||
|
minRequiredJobResults,
|
||||||
|
minRequiredOracleResults:
|
||||||
|
minRequiredOracleResults > batchSize
|
||||||
|
? batchSize
|
||||||
|
: minRequiredOracleResults,
|
||||||
|
minUpdateDelaySeconds,
|
||||||
|
name,
|
||||||
|
queueAccount,
|
||||||
|
startAfter,
|
||||||
|
varianceThreshold,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
const jobs: JobAccount[] = [];
|
||||||
|
if (aggregatorDefinition.jobs) {
|
||||||
|
for await (const job of aggregatorDefinition.jobs) {
|
||||||
|
const jobDefinition: fromJobJSON = JSON.parse(
|
||||||
|
JSON.stringify(job),
|
||||||
|
pubKeyConverter
|
||||||
|
);
|
||||||
|
const data = Buffer.from(
|
||||||
|
OracleJob.encodeDelimited(
|
||||||
|
OracleJob.create({
|
||||||
|
tasks: jobDefinition.tasks,
|
||||||
|
})
|
||||||
|
).finish()
|
||||||
|
);
|
||||||
|
|
||||||
|
const account = await JobAccount.create(this.program, {
|
||||||
|
data,
|
||||||
|
name: jobDefinition.name
|
||||||
|
? Buffer.from(jobDefinition.name)
|
||||||
|
: Buffer.from(""),
|
||||||
|
expiration: jobDefinition.expiration
|
||||||
|
? new anchor.BN(jobDefinition.expiration)
|
||||||
|
: new anchor.BN(0),
|
||||||
|
authority:
|
||||||
|
jobDefinition.authorityWalletPublicKey ?? payerKeypair.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
jobs.push(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for await (const job of jobs) {
|
||||||
|
await aggregatorAccount.addJob(job, authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.silent) {
|
||||||
|
this.logger.log(
|
||||||
|
await prettyPrintAggregator(
|
||||||
|
aggregatorAccount,
|
||||||
|
aggregator,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.outputFile) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
flags.outputFile,
|
||||||
|
JSON.stringify(aggregator, pubKeyConverter, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(aggregator.publicKey.toString());
|
||||||
|
} else {
|
||||||
|
this.logger.info(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator created successfully from JSON file\r\n`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to create aggregator from json file");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorLock extends BaseCommand {
|
||||||
|
static description =
|
||||||
|
"lock an aggregator's configuration and prevent further changes";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// static examples = ["$ sbv2 aggregator:set:authority"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorLock);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.lock(authority);
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(`${CHECK_ICON}Aggregator locked successfully\r\n`)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to lock aggregator configuration");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { prettyPrintPermissions } from "@switchboard-xyz/sbv2-utils";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
PermissionAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorPermissionCreate extends BaseCommand {
|
||||||
|
static description = "create a permission account for an aggregator";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args } = this.parse(AggregatorPermissionCreate);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
// assuming granter is an oracle queue, will need to fix
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: aggregator.queuePubkey,
|
||||||
|
});
|
||||||
|
const queue = await queueAccount.loadData();
|
||||||
|
|
||||||
|
// Check if permission account already exists
|
||||||
|
let permissionAccount: PermissionAccount;
|
||||||
|
try {
|
||||||
|
[permissionAccount] = PermissionAccount.fromSeed(
|
||||||
|
this.program,
|
||||||
|
queue.authority,
|
||||||
|
queueAccount.publicKey,
|
||||||
|
aggregatorAccount.publicKey
|
||||||
|
);
|
||||||
|
const permData = await permissionAccount.loadData();
|
||||||
|
if (!this.silent) {
|
||||||
|
this.logger.log(
|
||||||
|
`Permission Account already existed ${permissionAccount.publicKey}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
permissionAccount = await PermissionAccount.create(this.program, {
|
||||||
|
granter: queueAccount.publicKey,
|
||||||
|
grantee: aggregatorAccount.publicKey,
|
||||||
|
authority: queue.authority,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(permissionAccount.publicKey.toString());
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Permission account created successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(await prettyPrintPermissions(permissionAccount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to create permission account for aggregator");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount, JobAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorRemoveJob extends BaseCommand {
|
||||||
|
static description = "remove a switchboard job account from an aggregator";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
force: flags.boolean({
|
||||||
|
description: "overwrite outputFile if existing",
|
||||||
|
}),
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jobKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description:
|
||||||
|
"public key of an existing job account to remove from an aggregator",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = ["$ sbv2 aggregator:remove:job"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorRemoveJob);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const jobAccount = new JobAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: new PublicKey(args.jobKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.removeJob(jobAccount, authority);
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Job succesfully removed from aggregator account\r\n`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to remove job to aggregator account");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, loadKeypair } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetAuthority extends BaseCommand {
|
||||||
|
static description = "set an aggregator's authority";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
currentAuthority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newAuthority",
|
||||||
|
required: true,
|
||||||
|
description: "keypair path of new authority",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// static examples = ["$ sbv2 aggregator:set:authority"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetAuthority);
|
||||||
|
|
||||||
|
const newAuthority = await loadKeypair(args.newAuthority);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const currentAuthority = await this.loadAuthority(
|
||||||
|
flags.currentAuthority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setAuthority(
|
||||||
|
newAuthority.publicKey,
|
||||||
|
currentAuthority
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(`${CHECK_ICON}Aggregator authority set successfully`)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator authority");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetBatchSize extends BaseCommand {
|
||||||
|
static description = "set an aggregator's batch size";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "batchSize",
|
||||||
|
required: true,
|
||||||
|
description: "number of oracles requested for each open round call",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// static examples = ["$ sbv2 aggregator:set:authority"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetBatchSize);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const batchSize = Number.parseInt(args.batchSize, 10);
|
||||||
|
if (batchSize <= 0 || batchSize > 16) {
|
||||||
|
throw new Error(`Invalid batch size (1 - 16), ${batchSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
if (aggregator.minOracleResults > batchSize) {
|
||||||
|
throw new Error(
|
||||||
|
`Batch size ${batchSize} must be greater than minOracleResults ${aggregator.minOracleResults}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setBatchSize({
|
||||||
|
authority,
|
||||||
|
batchSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(`${CHECK_ICON}Aggregator batch size set successfully`)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator batch size");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetForceReportPeriod extends BaseCommand {
|
||||||
|
static description = "set an aggregator's force report period";
|
||||||
|
|
||||||
|
static aliases = ["aggregator:set:forceReport"];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forceReportPeriod",
|
||||||
|
required: true,
|
||||||
|
parse: (value: string) => Number.parseInt(value),
|
||||||
|
description:
|
||||||
|
"Number of seconds for which, even if the variance threshold is not passed, accept new responses from oracles.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:set:forceReportPeriod GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR 300 --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetForceReportPeriod);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await this.program.methods
|
||||||
|
.aggregatorSetForceReportPeriod({
|
||||||
|
forceReportPeriod: args.forceReportPeriod,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
})
|
||||||
|
.signers([authority])
|
||||||
|
.rpc();
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator force report period set successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator's force report period");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetHistoryBuffer extends BaseCommand {
|
||||||
|
static description =
|
||||||
|
"set an aggregator's history buffer account to record the last N accepted results";
|
||||||
|
|
||||||
|
static aliases = ["aggregator:add:history"];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator to add to a crank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size",
|
||||||
|
required: true,
|
||||||
|
parse: (value: string) => Number.parseInt(value, 10),
|
||||||
|
description: "size of history buffer",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:set:history GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR 10000 --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetHistoryBuffer);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const size = Number.parseInt(args.size, 10);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setHistoryBuffer({ authority, size });
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Added a history buffer of size ${size} to aggregator successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to add history buffer to aggregator");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey, Transaction } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
SwitchboardDecimal,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import Big from "big.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSet extends BaseCommand {
|
||||||
|
static description = "set an aggregator's config";
|
||||||
|
|
||||||
|
static aliases = ["set:aggregator"];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
forceReportPeriod: flags.string({
|
||||||
|
description:
|
||||||
|
"Number of seconds for which, even if the variance threshold is not passed, accept new responses from oracles.",
|
||||||
|
}),
|
||||||
|
// batchSize: flags.string({
|
||||||
|
// description: "number of oracles requested for each open round call",
|
||||||
|
// }),
|
||||||
|
minJobs: flags.string({
|
||||||
|
description: "number of jobs that must respond before an oracle responds",
|
||||||
|
}),
|
||||||
|
minOracles: flags.string({
|
||||||
|
description:
|
||||||
|
"number of oracles that must respond before a value is accepted on-chain",
|
||||||
|
}),
|
||||||
|
newQueue: flags.string({
|
||||||
|
description: "public key of the new oracle queue",
|
||||||
|
}),
|
||||||
|
updateInterval: flags.string({
|
||||||
|
description: "set an aggregator's minimum update delay",
|
||||||
|
}),
|
||||||
|
varianceThreshold: flags.string({
|
||||||
|
description:
|
||||||
|
"percentage change between a previous accepted result and the next round before an oracle reports a value on-chain. Used to conserve lease cost during low volatility",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:set GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR --updateInterval 300 --minOracles 3 --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSet);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const payerKeypair = programWallet(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = new Transaction({ feePayer: payerKeypair.publicKey });
|
||||||
|
|
||||||
|
// batch size
|
||||||
|
// if (flags.batchSize) {
|
||||||
|
// const batchSize = Number.parseInt(args.batchSize, 10);
|
||||||
|
// // if (batchSize <= 0 || batchSize > 16) {
|
||||||
|
// // throw new Error(`Invalid batch size (1 - 16), ${batchSize}`);
|
||||||
|
// // }
|
||||||
|
// // if (flags.minOracles && Number.parseInt(flags.minOracles) > batchSize) {
|
||||||
|
// // throw new Error(
|
||||||
|
// // `Batch size ${batchSize} must be greater than minOracleResults ${flags.minOracles}`
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// // if (
|
||||||
|
// // flags.minOracles === undefined &&
|
||||||
|
// // Number.parseInt(aggregator.minOracleResults) > batchSize
|
||||||
|
// // ) {
|
||||||
|
// // throw new Error(
|
||||||
|
// // `Batch size ${batchSize} must be greater than minOracleResults ${aggregator.minOracleResults}`
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// txn.add(
|
||||||
|
// await this.program.methods
|
||||||
|
// .aggregatorSetBatchSize({
|
||||||
|
// batchSize: batchSize,
|
||||||
|
// })
|
||||||
|
// .accounts({
|
||||||
|
// aggregator: aggregatorAccount.publicKey,
|
||||||
|
// authority: authority.publicKey,
|
||||||
|
// })
|
||||||
|
// .instruction()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// min oracles responses
|
||||||
|
if (flags.minOracles) {
|
||||||
|
const minOracles = Number.parseInt(flags.minOracles);
|
||||||
|
txn.add(
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorSetMinOracles({
|
||||||
|
minOracleResults: minOracles,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// min job responses
|
||||||
|
if (flags.minJobs) {
|
||||||
|
const minJobs = Number.parseInt(flags.minJobs);
|
||||||
|
txn.add(
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorSetMinJobs({
|
||||||
|
minJobResults: minJobs,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// oracle queue
|
||||||
|
if (flags.newQueue) {
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: new PublicKey(flags.newQueue),
|
||||||
|
});
|
||||||
|
txn.add(
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorSetQueue({})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
queue: queueAccount.publicKey,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// force report period
|
||||||
|
if (flags.forceReportPeriod) {
|
||||||
|
txn.add(
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorSetForceReportPeriod({
|
||||||
|
forceReportPeriod: Number.parseInt(flags.forceReportPeriod),
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// variance threshold
|
||||||
|
if (flags.varianceThreshold) {
|
||||||
|
const varianceThreshold = new Big(flags.varianceThreshold);
|
||||||
|
txn.add(
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorSetVarianceThreshold({
|
||||||
|
varianceThreshold: SwitchboardDecimal.fromBig(varianceThreshold),
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update interval
|
||||||
|
if (flags.updateInterval) {
|
||||||
|
const updateInterval = Number.parseInt(flags.updateInterval, 10);
|
||||||
|
txn.add(
|
||||||
|
await this.program.methods
|
||||||
|
.aggregatorSetUpdateInterval({
|
||||||
|
newInterval: updateInterval,
|
||||||
|
})
|
||||||
|
.accounts({
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
})
|
||||||
|
.instruction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = await this.program.provider.sendAndConfirm(txn, [
|
||||||
|
payerKeypair,
|
||||||
|
authority,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(signature);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator force report period set successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${signature}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator's config");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetMinJobResults extends BaseCommand {
|
||||||
|
static description =
|
||||||
|
"set an aggregator's minimum number of jobs before an oracle responds";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minJobResults",
|
||||||
|
required: true,
|
||||||
|
description: "number of jobs that must respond before an oracle responds",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// static examples = ["$ sbv2 aggregator:set:authority"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetMinJobResults);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const minJobResults = Number.parseInt(args.minJobResults, 10);
|
||||||
|
if (minJobResults <= 0 || minJobResults > 16) {
|
||||||
|
throw new Error(`Invalid min job size (1 - 16), ${minJobResults}`);
|
||||||
|
}
|
||||||
|
if (minJobResults > aggregator.jobPubkeysSize) {
|
||||||
|
throw new Error(
|
||||||
|
`Min jobs ${minJobResults} is greater than current number of jobs ${aggregator.jobPubkeysSize} `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setMinJobs({
|
||||||
|
authority,
|
||||||
|
minJobResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(`${CHECK_ICON}Aggregator min job set successfully`)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator min jobs");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetMinOracleResults extends BaseCommand {
|
||||||
|
static description =
|
||||||
|
"set an aggregator's minimum number of oracles that must respond before a result is accepted on-chain";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minOracleResults",
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
"number of oracles that must respond before a value is accepted on-chain",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// static examples = ["$ sbv2 aggregator:set:authority"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetMinOracleResults);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
const minOracleResults = Number.parseInt(args.minOracleResults, 10);
|
||||||
|
if (minOracleResults <= 0 || minOracleResults > 16) {
|
||||||
|
throw new Error(`Invalid min oracle size (1 - 16), ${minOracleResults}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setMinOracles({
|
||||||
|
authority,
|
||||||
|
minOracleResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator minimum oracles set successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator minimum oracles");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetQueue extends BaseCommand {
|
||||||
|
static description = "set an aggregator's oracle queue";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "queueKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the oracle queue",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetQueue);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
const oracleQueue = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.queueKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await this.program.rpc.aggregatorSetQueue(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
accounts: {
|
||||||
|
aggregator: aggregatorAccount.publicKey,
|
||||||
|
authority: authority.publicKey,
|
||||||
|
queue: oracleQueue.publicKey,
|
||||||
|
},
|
||||||
|
signers: [authority],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator oracle queue set successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator's history buffer");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetUpdateInterval extends BaseCommand {
|
||||||
|
static description = "set an aggregator's minimum update delay";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updateInterval",
|
||||||
|
required: true,
|
||||||
|
parse: (newInterval: string) => Number.parseInt(newInterval, 10),
|
||||||
|
description: "set an aggregator's minimum update delay",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:set:updateInterval GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR 60 --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetUpdateInterval);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
if (aggregator.minUpdateDelaySeconds === args.updateInterval) {
|
||||||
|
throw new Error(
|
||||||
|
`Aggregator already has a minUpdateDelaySeconds of ${args.updateInterval}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.updateInterval < 5) {
|
||||||
|
throw new Error(
|
||||||
|
`Update interval should be greater than 5 seconds, ${args.updateInterval}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setUpdateInterval({
|
||||||
|
newInterval: args.updateInterval,
|
||||||
|
authority,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator minimum update delay set successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator minimum update delay");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { AggregatorAccount } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import Big from "big.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorSetVarianceThreshold extends BaseCommand {
|
||||||
|
static description = "set an aggregator's variance threshold";
|
||||||
|
|
||||||
|
static aliases = ["aggregator:set:variance"];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
authority: flags.string({
|
||||||
|
char: "a",
|
||||||
|
description: "alternate keypair that is the authority for the aggregator",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "varianceThreshold",
|
||||||
|
required: true,
|
||||||
|
parse: (variance: string) => new Big(variance),
|
||||||
|
description:
|
||||||
|
"percentage change between a previous accepted result and the next round before an oracle reports a value on-chain. Used to conserve lease cost during low volatility",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:set:varianceThreshold GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR 0.1 --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(AggregatorSetVarianceThreshold);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
const authority = await this.loadAuthority(
|
||||||
|
flags.authority,
|
||||||
|
aggregator.authority
|
||||||
|
);
|
||||||
|
|
||||||
|
const txn = await aggregatorAccount.setVarianceThreshold({
|
||||||
|
authority,
|
||||||
|
threshold: args.varianceThreshold,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator variance threshold set successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set aggregator's variance threshold");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
import { AggregatorIllegalRoundOpenCall } from "../../types";
|
||||||
|
import { CHECK_ICON } from "../../utils";
|
||||||
|
|
||||||
|
export default class AggregatorUpdate extends BaseCommand {
|
||||||
|
static description = "request a new aggregator result from a set of oracles";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator account to deserialize",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 aggregator:update J7j9xX8JP2B2ErvUzuqGAKBGeggsxPyFXj5MqZcYDxfa --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args } = this.parse(AggregatorUpdate);
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
const aggregator = await aggregatorAccount.loadData();
|
||||||
|
|
||||||
|
const oracleQueueAccount = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: aggregator.queuePubkey,
|
||||||
|
});
|
||||||
|
const queue = await oracleQueueAccount.loadData();
|
||||||
|
|
||||||
|
const mint = await oracleQueueAccount.loadMint();
|
||||||
|
|
||||||
|
const payoutWallet = (
|
||||||
|
await mint.getOrCreateAssociatedAccountInfo(
|
||||||
|
programWallet(this.program).publicKey
|
||||||
|
)
|
||||||
|
).address;
|
||||||
|
|
||||||
|
const aggregatorUpdateTxn = await aggregatorAccount.openRound({
|
||||||
|
oracleQueueAccount,
|
||||||
|
payoutWallet,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(aggregatorUpdateTxn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator update request sent to oracles`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://solscan.io/tx/${aggregatorUpdateTxn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
if (
|
||||||
|
error instanceof AggregatorIllegalRoundOpenCall ||
|
||||||
|
error.toString().includes("0x177d")
|
||||||
|
) {
|
||||||
|
this.context.logger.info(error.toString());
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
super.catch(error, "failed to open a new aggregator update round");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import BaseCommand from "../BaseCommand";
|
||||||
|
import OracleDeposit from "./oracle/deposit";
|
||||||
|
import OracleWithdraw from "./oracle/withdraw";
|
||||||
|
|
||||||
|
export default class Config extends BaseCommand {
|
||||||
|
static hidden = true; // not ready yet
|
||||||
|
|
||||||
|
baseCommand: string;
|
||||||
|
|
||||||
|
flags: any;
|
||||||
|
|
||||||
|
passThroughArguments: string[];
|
||||||
|
|
||||||
|
static description = "run a cli command using your saved configuration";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
pubkey: flags.string({
|
||||||
|
description:
|
||||||
|
"command specific. override default account with provided public key",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "baseCommand",
|
||||||
|
required: true,
|
||||||
|
description: "base command to run",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "passThroughArguments",
|
||||||
|
required: false,
|
||||||
|
description: "pass through arguements for baseCommand",
|
||||||
|
}, // documentation purposes
|
||||||
|
];
|
||||||
|
|
||||||
|
static strict = false;
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await super.init();
|
||||||
|
const { args, flags, argv } = this.parse(Config);
|
||||||
|
this.passThroughArguments = argv.slice(1);
|
||||||
|
this.baseCommand = args.baseCommand;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
switch (this.baseCommand) {
|
||||||
|
case "oracle:withdraw": {
|
||||||
|
await OracleWithdraw.run(this.passThroughArguments);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "oracle:deposit": {
|
||||||
|
await OracleDeposit.run(this.passThroughArguments);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { chalkString } from "../../accounts/utils";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
|
||||||
|
export default class ConfigPrint extends BaseCommand {
|
||||||
|
static description = "print cli config";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
static examples = ["$ sbv2 config:print"];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { devnet, mainnet } = this.cliConfig;
|
||||||
|
this.log(chalk.underline(chalk.blue("## Mainnet-Beta".padEnd(16))), "info");
|
||||||
|
this.log(chalkString("mainnet-rpc", mainnet.rpcUrl || "N/A"), "info");
|
||||||
|
this.log(chalk.underline(chalk.blue("## Devnet".padEnd(16))), "info");
|
||||||
|
this.log(chalkString("devnet-rpc", devnet.rpcUrl || "N/A"), "info");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
import { ConfigParameter } from "../../config";
|
||||||
|
|
||||||
|
export default class ConfigSet extends BaseCommand {
|
||||||
|
hidden = true;
|
||||||
|
|
||||||
|
static description = "set a configuration option";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
reset: flags.boolean({
|
||||||
|
char: "r",
|
||||||
|
description: "remove value or set to default rpc",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "param",
|
||||||
|
required: true,
|
||||||
|
options: ["devnet-rpc", "mainnet-rpc"],
|
||||||
|
parse: (string_: string) => string_ as ConfigParameter,
|
||||||
|
description: "configuration parameter to set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value",
|
||||||
|
required: false,
|
||||||
|
description: "value of the param to set",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(ConfigSet);
|
||||||
|
this.setConfig(args.param, flags.reset ? undefined : args.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to set config option");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { CrankAccount, CrankRow } from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
|
||||||
|
export default class CrankList extends BaseCommand {
|
||||||
|
static description = "list the pubkeys currently on the crank";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
force: flags.boolean({ description: "overwrite output file if exists" }),
|
||||||
|
outputFile: flags.string({
|
||||||
|
char: "f",
|
||||||
|
description: "output file to save aggregator pubkeys to",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "crankKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the crank",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args, flags } = this.parse(CrankList);
|
||||||
|
|
||||||
|
const outputFile = flags.outputFile
|
||||||
|
? path.join(process.cwd(), flags.outputFile)
|
||||||
|
: undefined;
|
||||||
|
if (outputFile && fs.existsSync(outputFile) && !flags.force) {
|
||||||
|
throw new Error(
|
||||||
|
`${outputFile} already exists, use the --force flag to overwrite`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const crankAccount = new CrankAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.crankKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const crank = await crankAccount.loadData();
|
||||||
|
const pqData: CrankRow[] = crank.pqData;
|
||||||
|
|
||||||
|
const pqKeys = pqData.map((row) => row.pubkey.toString());
|
||||||
|
|
||||||
|
if (outputFile) {
|
||||||
|
if (outputFile.endsWith(".txt")) {
|
||||||
|
fs.writeFileSync(outputFile, pqKeys.join("\n"));
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(
|
||||||
|
outputFile,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
crank: crankAccount.publicKey.toString(),
|
||||||
|
pubkeys: pqKeys,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.silent) {
|
||||||
|
this.logger.log(pqKeys.join("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to print the cranks pubkeys");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
AggregatorAccount,
|
||||||
|
CrankAccount,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../utils";
|
||||||
|
|
||||||
|
export default class CrankPush extends BaseCommand {
|
||||||
|
static description = "push an aggregator onto a crank";
|
||||||
|
|
||||||
|
static aliases = ["aggregator:add:crank", "crank:add:aggregator"];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "crankKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the crank",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "aggregatorKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the aggregator",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args } = this.parse(CrankPush);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
|
||||||
|
const crankAccount = new CrankAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.crankKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const aggregatorAccount = new AggregatorAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.aggregatorKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const txn = await crankAccount.push({ aggregatorAccount });
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(
|
||||||
|
`${CHECK_ICON}Aggregator pushed to crank successfully`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to push aggregator onto the crank");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
CrankAccount,
|
||||||
|
OracleQueueAccount,
|
||||||
|
ProgramStateAccount,
|
||||||
|
programWallet,
|
||||||
|
} from "@switchboard-xyz/switchboard-v2";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import BaseCommand from "../../BaseCommand";
|
||||||
|
import { CHECK_ICON, verifyProgramHasPayer } from "../../utils";
|
||||||
|
|
||||||
|
export default class CrankTurn extends BaseCommand {
|
||||||
|
crankAccount: CrankAccount;
|
||||||
|
|
||||||
|
static description =
|
||||||
|
"turn the crank and get rewarded if aggregator updates available";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
...BaseCommand.flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
static args = [
|
||||||
|
{
|
||||||
|
name: "crankKey",
|
||||||
|
required: true,
|
||||||
|
parse: (pubkey: string) => new PublicKey(pubkey),
|
||||||
|
description: "public key of the crank to turn",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
"$ sbv2 crank:turn 85L2cFUvXaeGQ4HrzP8RJEVCL7WvRrXM2msvEmQ82AVr --keypair ../payer-keypair.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { args } = this.parse(CrankTurn);
|
||||||
|
verifyProgramHasPayer(this.program);
|
||||||
|
const payer = programWallet(this.program);
|
||||||
|
|
||||||
|
// load crank
|
||||||
|
const crankAccount = new CrankAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: args.crankKey,
|
||||||
|
});
|
||||||
|
const crank = await crankAccount.loadData();
|
||||||
|
|
||||||
|
// load queue
|
||||||
|
const queueAccount = new OracleQueueAccount({
|
||||||
|
program: this.program,
|
||||||
|
publicKey: crank.queuePubkey,
|
||||||
|
});
|
||||||
|
const queue = await queueAccount.loadData();
|
||||||
|
|
||||||
|
// load program state
|
||||||
|
const [programStateAccount] = ProgramStateAccount.fromSeed(this.program);
|
||||||
|
const progamState = await programStateAccount.loadData();
|
||||||
|
|
||||||
|
// get payer payout wallet
|
||||||
|
const switchboardMint = await programStateAccount.getTokenMint();
|
||||||
|
const payoutWalletAccountInfo =
|
||||||
|
await switchboardMint.getOrCreateAssociatedAccountInfo(payer.publicKey);
|
||||||
|
|
||||||
|
const txn = await crankAccount.pop({
|
||||||
|
payoutWallet: payoutWalletAccountInfo.address,
|
||||||
|
queuePubkey: queueAccount.publicKey,
|
||||||
|
queueAuthority: queue.authority,
|
||||||
|
crank: 0,
|
||||||
|
queue: 0,
|
||||||
|
tokenMint: progamState.tokenMintPublicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.silent) {
|
||||||
|
console.log(txn);
|
||||||
|
} else {
|
||||||
|
this.logger.log(
|
||||||
|
`${chalk.green(`${CHECK_ICON}Crank turned successfully`)}`
|
||||||
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`https://explorer.solana.com/tx/${txn}?cluster=${this.cluster}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error) {
|
||||||
|
super.catch(error, "failed to turn the crank");
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue