chore: pull monorepo changes (#111)

* Project import generated by Copybara.

GitOrigin-RevId: 8a02570f92a45d5127c2437ea4ceb1867f1da725

* chore: Update pnpm lockfile

---------

Co-authored-by: Copybara <copybara@example.com>
Co-authored-by: gallynaut <gallynaut@users.noreply.github.com>
This commit is contained in:
gallynaut 2023-07-19 09:23:43 -06:00 committed by GitHub
parent 0b5e0911a1
commit 84c2abc1f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
320 changed files with 39701 additions and 178772 deletions

View File

@ -4227,9 +4227,9 @@ checksum = "ab16ced94dbd8a46c82fd81e3ed9a8727dac2977ea869d217bcc4ea1f122e81f"
[[package]]
name = "switchboard-common"
version = "0.8.5"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a03303674fb73b1dbe6cfc83aa8faa0e99264472a47177a1803f7475b318d1"
checksum = "1ba2cc1d6055e0989756adc4f54fa1e27d684ce08d6164da23900e93ac0b13b6"
dependencies = [
"getrandom 0.2.10",
"hex",
@ -4249,7 +4249,7 @@ dependencies = [
[[package]]
name = "switchboard-solana"
version = "0.9.1"
version = "0.8.4"
dependencies = [
"anchor-client",
"anchor-lang",
@ -4258,7 +4258,6 @@ dependencies = [
"bytemuck",
"chrono",
"cron",
"hex",
"rust_decimal",
"sgx-quote",
"solana-address-lookup-table-program",
@ -4266,7 +4265,6 @@ dependencies = [
"solana-program",
"superslice",
"switchboard-common",
"tokio",
]
[[package]]

View File

@ -17,5 +17,5 @@ cpi = ["no-entrypoint"]
[dependencies]
# switchboard-solana = "0.8.4"
switchboard-solana = { version = "0.9.1", path = "../../../rust/switchboard-solana" }
switchboard-solana = { version = "0.8.4", path = "../../../rust/switchboard-solana" }
bytemuck = "1.13.1"

View File

@ -8,11 +8,9 @@
"directory": "examples/feeds/01_feed_client"
},
"scripts": {
"lint": "eslint --ext .js,.json,.ts 'src/**' --fix",
"build:old": "node ../build.js anchor-vrf-parser 4hfkS97f5P6zauWeJF5dcZDFaJwYwbMPvKrANVAuKN4p",
"build": "anchor build",
"test": "npm run localnet && npm run network:create && npm run network:start & sleep 60 && anchor test --skip-local-validator",
"test:dev": "npm run localnet && npm run network:create && npm run network:start:dev & sleep 15 && anchor test --skip-local-validator"
"build:cargo": "anchor build",
"fix": "cargo fmt && pnpm exec prettier ./tests/*.ts -w",
"clean": "pnpm exec rimraf node_modules .anchor .turbo"
},
"dependencies": {
"@coral-xyz/anchor": "^0.28.0",
@ -20,9 +18,9 @@
"@project-serum/borsh": "^0.2.5",
"@solana/spl-token": "^0.3.6",
"@solana/web3.js": "^1.77.3",
"@switchboard-xyz/common": "^2.2.4",
"@switchboard-xyz/oracle": "^2.1.13",
"@switchboard-xyz/solana.js": "workspace:*",
"@switchboard-xyz/common": "latest",
"@switchboard-xyz/oracle": "latest",
"@switchboard-xyz/solana.js": "latest",
"chalk": "^4.1.2",
"dotenv": "^16.0.1",
"yargs": "^17.5.1"
@ -30,18 +28,10 @@
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^17.0.45",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"chai": "^4.3.6",
"eslint": "^8.28.0",
"mocha": "^9.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"prettier-plugin-organize-imports": "^2.3.4",
"shx": "^0.3.4",
"ts-mocha": "^9.0.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
"ts-mocha": "^9.0.2"
}
}

View File

@ -5,14 +5,8 @@
}
},
"compilerOptions": {
"types": [
"mocha",
"chai",
"node"
],
"typeRoots": [
"./node_modules/@types"
],
"types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"],
"module": "commonjs",
"noEmit": true,
"esModuleInterop": true,
@ -20,23 +14,14 @@
"strictNullChecks": false,
"target": "es6",
"paths": {
"@switchboard-xyz/solana.js": [
"../../../javascript/solana.js"
]
"@switchboard-xyz/solana.js": ["../../../javascript/solana.js"]
}
},
"include": [
"tests/**/*",
"./cli.ts",
"./client/**/*"
],
"exclude": [
"target",
"lib"
],
"include": ["tests/**/*", "./cli.ts", "./client/**/*"],
"exclude": ["target", "lib"],
"references": [
{
"path": "../../../javascript/solana.js"
}
]
}
}

View File

@ -12,4 +12,4 @@ no-entrypoint = []
[dependencies]
# switchboard-solana = "0.8.4"
switchboard-solana = { version = "0.9.1", path = "../../../rust/switchboard-solana" }
switchboard-solana = { version = "0.8.4", path = "../../../rust/switchboard-solana" }

View File

@ -8,25 +8,24 @@
"directory": "programs/native-feed-parser"
},
"scripts": {
"build:bpf": "cargo-build-sbf",
"build:cargo": "cargo-build-sbf",
"fix": "cargo fmt && pnpm exec prettier ./tests/*.ts -w",
"clean": "pnpm exec rimraf node_modules .anchor .turbo",
"deploy": "solana program deploy target/deploy/native_feed_parser.so",
"test": "echo \"For workspace native-feed-parser, use the anchor:test script\" && exit 0"
"test:program": "echo \"For workspace native-feed-parser, use the anchor:test script\" && exit 0"
},
"dependencies": {
"@coral-xyz/anchor": "^0.28.0",
"@solana/web3.js": "^1.77.3",
"@switchboard-xyz/common": "^2.2.4",
"@switchboard-xyz/oracle": "^2.1.13",
"@switchboard-xyz/solana.js": "workspace:*"
"@switchboard-xyz/common": "latest",
"@switchboard-xyz/oracle": "latest",
"@switchboard-xyz/solana.js": "latest"
},
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^17.0.45",
"chai": "^4.3.6",
"mocha": "^9.0.3",
"ts-mocha": "^9.0.2",
"ts-node": "^10.4.0",
"typescript": "^4.7"
"ts-mocha": "^9.0.2"
}
}

View File

@ -1,31 +1,20 @@
{
"compilerOptions": {
"types": [
"mocha",
"chai"
],
"typeRoots": [
"./node_modules/@types"
],
"lib": [
"es2015"
],
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"noEmit": true,
"paths": {
"@switchboard-xyz/solana.js": [
"../../../javascript/solana.js"
]
"@switchboard-xyz/solana.js": ["../../../javascript/solana.js"]
}
},
"exclude": [
"target"
],
"exclude": ["target"],
"references": [
{
"path": "../../../javascript/solana.js"
}
]
}
}

View File

@ -3,4 +3,8 @@ Makefile
README.md
node_modules
.turbo
measurement.txt
measurement.txt
.anchor
scripts
.prettierignore
.gitignore

View File

@ -6,14 +6,18 @@ seeds = false
skip-lint = false
[programs.localnet]
basic_oracle = "8GHcguBXZEfKaLxRrBvis7LqPcFjBszv4ZYBgKwCUipS"
basic_oracle = "BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX"
[programs.devnet]
basic_oracle = "BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX"
[provider]
# cluster = "https://switchbo-switchbo-6225.devnet.rpcpool.com/f6fb9f02-0777-498b-b8f5-67cbb1fc0d14"
# cluster = "https://api.devnet.solana.com"
# wallet = "~/switchboard_environments_v2/devnet/upgrade_authority/upgrade_authority.json"
cluster = "Localnet"
wallet = "~/.config/solana/id.json"
# cluster = "devnet"
[scripts]
test = "pnpm exec ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
@ -42,4 +46,4 @@ address = "sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx"
address = "5ExuoQR69trmKQfB95fDsUGsUrrChbGq9PFgt8qouncz"
[[test.validator.clone]] # sb devnet attestation State
address = "BzqtGXZPiDSinP4xMFgPf6FLgSa6iPufK4m4JJFgMnTK"
address = "5MFs7RGTjLi1wtKNBFRtuLipCkkjs4YQwRRU9sjnbQbS"

View File

@ -18,7 +18,7 @@ default = []
[dependencies]
switchboard-solana = { version = "=0.9.1" }
# switchboard-solana = { version = "0.9.1", path = "../../../../../rust/switchboard-solana" }
# switchboard-solana = { version = "0.9.1", path = "../../../rust/switchboard-solana" }
bytemuck = "^1"
anchor-lang = { version = "0.28.0", features = [
"init-if-needed",

View File

@ -0,0 +1,37 @@
<div align="center">
![Switchboard Logo](https://github.com/switchboard-xyz/sbv2-core/raw/main/website/static/img/icons/switchboard/avatar.png)
# anchor-vrf-lite-parser
> An example program written in Anchor demonstrating how to integrate Switchboard Functions and verify attestation on-chain.
[![Anchor Test Status](https://github.com/switchboard-xyz/sbv2-solana/actions/workflows/anchor-test.yml/badge.svg)](https://github.com/switchboard-xyz/sbv2-solana/actions/workflows/anchor-test.yml)
</div>
<!-- install -->
<!-- installstop -->
## Usage
Build the example program
```bash
anchor build
```
Get your program ID and update `Anchor.toml` and `src/lib.rs` with your pubkey
```bash
export ANCHOR_VRF_LITE_PARSER_PUBKEY=$(solana-keygen pubkey target/deploy/anchor_vrf_lite_parser-keypair.json)
sed -i '' s/5Hhm5xKDiThfidbpqjJpKmMJEcKmjj5tEUNFpi2DzSvb/"$ANCHOR_VRF_LITE_PARSER_PUBKEY"/g Anchor.toml
sed -i '' s/5Hhm5xKDiThfidbpqjJpKmMJEcKmjj5tEUNFpi2DzSvb/"$ANCHOR_VRF_LITE_PARSER_PUBKEY"/g src/lib.rs
```
Then run Anchor test
```bash
anchor test
```

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
#!/bin/bash
curl https://api.binance.com/api/v3/exchangeInfo

View File

@ -1,5 +1,5 @@
{
"name": "basic-oracle",
"name": "solana-basic-oracle",
"private": true,
"repository": {
"type": "git",
@ -7,16 +7,17 @@
"directory": "examples/functions/01_basic_oracle"
},
"scripts": {
"build:anchor": "anchor build",
"clean": "pnpm exec rimraf node_modules .anchor lib .turbo"
"build:cargo": "anchor build",
"fix": "cargo fmt && pnpm exec prettier ./tests/*.ts -w",
"clean": "pnpm exec rimraf node_modules .anchor .turbo"
},
"dependencies": {
"@coral-xyz/anchor": "^0.28.0",
"@solana/spl-token": "^0.3.6",
"@solana/web3.js": "^1.78.0",
"@switchboard-xyz/common": "*",
"@switchboard-xyz/oracle": "*",
"@switchboard-xyz/solana.js": "workspace:*"
"@switchboard-xyz/common": "latest",
"@switchboard-xyz/oracle": "latest",
"@switchboard-xyz/solana.js": "latest"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",

View File

@ -18,4 +18,4 @@ target/
/target
measurement.txt
*measurement.txt

File diff suppressed because it is too large Load Diff

View File

@ -14,4 +14,4 @@ futures = "0.3"
serde = "^1"
serde_json = "^1"
switchboard-utils = { version = "0.8.0" }
switchboard-solana = { version = "=0.9.1" }
switchboard-solana = { version = "0.9.1", path = "../../../rust/switchboard-solana" }

View File

@ -1,19 +1,25 @@
# syntax=docker/dockerfile:1.4
FROM switchboardlabs/sgx-function AS builder
FROM switchboardlabs/sgx-function:main AS builder
ARG CARGO_NAME=switchboard-function
ENV CARGO_NAME=$CARGO_NAME
WORKDIR /home/root/switchboard-function
COPY . .
COPY ./Anchor.toml ./Cargo.lock ./Cargo.toml ./
COPY ./src ./src
WORKDIR /home/root/switchboard-function/sgx-function
COPY ./sgx-function/Cargo.lock ./sgx-function/Cargo.toml ./
COPY ./sgx-function/src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry --mount=type=cache,target=/home/root/switchboard-function/sgx-function/target \
RUN --mount=target=/home/root/.cargo/git,type=cache \
--mount=target=/home/root/.cargo/registry,type=cache \
--mount=type=cache,target=/home/root/switchboard-function/sgx-function/target \
cargo build --release && \
cargo strip && \
mv target/release/basic-oracle-function /sgx/app
FROM switchboardlabs/sgx-function
FROM switchboardlabs/sgx-function:main
# Copy the binary
WORKDIR /sgx
@ -22,7 +28,4 @@ COPY --from=builder /sgx/app /sgx
# Get the measurement from the enclave
RUN /get_measurement.sh
RUN cp /app.manifest.sgx /sgx/app.manifest.sgx
RUN cp /app.sig /sgx/app.sig
ENTRYPOINT ["bash", "/boot.sh"]

View File

@ -2,7 +2,7 @@
# Variables
CARGO_NAME=basic-oracle-function
DOCKER_IMAGE_NAME=switchboardlabs/basic-oracle-function
DOCKER_IMAGE_NAME=gallynaut/binance-oracle
DOCKER_BUILD_COMMAND=DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 --build-arg CARGO_NAME=${CARGO_NAME}
@ -10,14 +10,14 @@ DOCKER_BUILD_COMMAND=DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd6
all: build
docker_build:
${DOCKER_BUILD_COMMAND} -f Dockerfile -t ${DOCKER_IMAGE_NAME} --load ../
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile -t ${DOCKER_IMAGE_NAME}:dev --load ../
docker_publish:
${DOCKER_BUILD_COMMAND} -f Dockerfile -t ${DOCKER_IMAGE_NAME} --push ../
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile -t ${DOCKER_IMAGE_NAME} --push ../
dev_docker_build:
${DOCKER_BUILD_COMMAND} -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME} --load ../../../../
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME}:dev --load ../../../../
dev_docker_publish:
${DOCKER_BUILD_COMMAND} -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME} --push ../../../../
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME} --push ../../../../
build: docker_build measurement
@ -25,12 +25,22 @@ dev: dev_docker_build measurement
publish: docker_publish measurement
latest_mr_enclave:
@docker run -d --pull=always --platform=linux/amd64 --name=latest-my-switchboard-function ${DOCKER_IMAGE_NAME}:latest > /dev/null
@docker cp latest-my-switchboard-function:/measurement.txt latest-measurement.txt
@docker stop latest-my-switchboard-function > /dev/null
@docker rm latest-my-switchboard-function > /dev/null
echo "latest MrEnclave: $(cat measurement.txt)"
measurement:
@docker run -d --name my-switchboard-function $(DOCKER_IMAGE_NAME) > /dev/null
@docker run -d --platform=linux/amd64 --name=my-switchboard-function ${DOCKER_IMAGE_NAME}:dev > /dev/null
@docker cp my-switchboard-function:/measurement.txt measurement.txt
@docker stop my-switchboard-function > /dev/null
@docker rm my-switchboard-function > /dev/null
simulate: docker_build
docker run -it --platform=linux/amd64 --entrypoint=/bin/bash ${DOCKER_IMAGE_NAME}:dev /boot.sh --test
# Task to clean up the compiled rust application
clean:
cargo clean

View File

@ -1,9 +1,10 @@
{
"name": "basic-oracle-function",
"scripts": {
"build:function": "make"
"build:cargo": "make",
"fix": "cargo fmt"
},
"dependencies": {
"basic-oracle": "workspace:*"
"solana-basic-oracle": "latest"
}
}

View File

@ -4,8 +4,8 @@ use crate::*;
pub use switchboard_utils::reqwest;
use basic_oracle::{OracleDataBorsh, TradingSymbol};
use serde::Deserialize;
use basic_oracle::{TradingSymbol, OracleDataBorsh};
const ONE: i128 = 1000000000;
@ -153,12 +153,12 @@ impl Binance {
data: self.eth_usdt.clone().into(),
},
// OracleDataWithTradingSymbol {
// symbol: TradingSymbol::Sol,
// data: self.sol_usdt.clone().into(),
// symbol: TradingSymbol::Sol,
// data: self.sol_usdt.clone().into(),
// },
// OracleDataWithTradingSymbol {
// symbol: TradingSymbol::Doge,
// data: self.doge_usdt.clone().into(),
// symbol: TradingSymbol::Doge,
// data: self.doge_usdt.clone().into(),
// },
];
@ -189,7 +189,11 @@ impl Binance {
is_writable: false,
},
],
data: [ix_discriminator("refresh_oracles").to_vec(), params.try_to_vec().unwrap()].concat(),
data: [
ix_discriminator("refresh_oracles").to_vec(),
params.try_to_vec().unwrap(),
]
.concat(),
};
vec![ixn]
@ -201,4 +205,4 @@ pub fn parse_string_value(value: &str) -> i128 {
let f64_value = value.parse::<f64>().unwrap();
let sb_decimal = SwitchboardDecimal::from_f64(f64_value);
sb_decimal.scale_to(9)
}
}

View File

@ -50,4 +50,4 @@ impl Initialize<'_> {
oracle.bump = *ctx.bumps.get("oracle").unwrap();
Ok(())
}
}
}

View File

@ -8,4 +8,4 @@ pub mod set_function;
pub use set_function::*;
pub mod trigger_function;
pub use trigger_function::*;
pub use trigger_function::*;

View File

@ -45,4 +45,4 @@ impl RefreshPrices<'_> {
Ok(())
}
}
}

View File

@ -18,7 +18,7 @@ pub struct SetFunction<'info> {
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct SetFunctionParams { }
pub struct SetFunctionParams {}
impl SetFunction<'_> {
pub fn validate(
@ -34,4 +34,4 @@ impl SetFunction<'_> {
program.function = ctx.accounts.function.key();
Ok(())
}
}
}

View File

@ -21,7 +21,7 @@ pub struct TriggerFunction<'info> {
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct TriggerFunctionParams { }
pub struct TriggerFunctionParams {}
impl TriggerFunction<'_> {
pub fn validate(
@ -32,12 +32,16 @@ impl TriggerFunction<'_> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &TriggerFunctionParams) -> anchor_lang::Result<()> {
pub fn actuate(
ctx: &Context<Self>,
_params: &TriggerFunctionParams,
) -> anchor_lang::Result<()> {
FunctionTrigger {
function: ctx.accounts.function.clone(),
authority: ctx.accounts.authority.clone(),
attestation_queue: ctx.accounts.attestation_queue.clone(),
}.invoke(ctx.accounts.attestation_program.clone())?;
}
.invoke(ctx.accounts.attestation_program.clone())?;
Ok(())
}
}
}

View File

@ -1,7 +1,7 @@
pub use switchboard_solana::prelude::*;
pub mod actions;
pub use actions::*;
pub use switchboard_solana::prelude::*;
declare_id!("EF68PJkRqQu2VthTSy19kg6TWynMtRmLpxcMDKEdLC8t");
pub mod error;
pub use error::*;
@ -12,7 +12,8 @@ pub use model::*;
pub mod utils;
pub use utils::*;
// IDL 51F8RoK1RcduTxD8KsFGn4LUuHFnPTCf2PdAF5qEYoMU
declare_id!("BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX");
pub const PROGRAM_SEED: &[u8] = b"BASICORACLE";
@ -53,4 +54,4 @@ pub mod basic_oracle {
) -> anchor_lang::Result<()> {
TriggerFunction::actuate(&ctx, &params)
}
}
}

View File

@ -0,0 +1,10 @@
target/
Makefile
README.md
node_modules
.turbo
measurement.txt
.anchor
scripts
.prettierignore
.gitignore

View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
**/*.rs.bk
node_modules
test-ledger
.yarn

View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
node_modules
dist
build
test-ledger

View File

@ -0,0 +1,49 @@
[workspace]
members = ["."]
[features]
seeds = false
skip-lint = false
[programs.localnet]
basic_oracle = "BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX"
[programs.devnet]
basic_oracle = "BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX"
[provider]
# cluster = "https://api.devnet.solana.com"
# wallet = "~/switchboard_environments_v2/devnet/upgrade_authority/upgrade_authority.json"
cluster = "Localnet"
wallet = "~/.config/solana/id.json"
# cluster = "devnet"
[scripts]
test = "pnpm exec ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
[test]
startup_wait = 15000
[test.validator]
url = "https://api.devnet.solana.com"
[[test.validator.clone]] # sb devnet oracle programID
address = "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
[[test.validator.clone]] # sb devnet oracle IDL
address = "Fi8vncGpNKbq62gPo56G4toCehWNy77GgqGkTaAF5Lkk"
[[test.validator.clone]] # sb devnet oracle SbState
address = "CyZuD7RPDcrqCGbNvLCyqk6Py9cEZTKmNKujfPi3ynDd"
[[test.validator.clone]] # sb devnet oracle tokenVault
address = "7hkp1xfPBcD2t1vZMoWWQPzipHVcXeLAAaiGXdPSfDie"
[[test.validator.clone]] # sb devnet attestation programID
address = "sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx"
[[test.validator.clone]] # sb devnet attestation IDL
address = "5ExuoQR69trmKQfB95fDsUGsUrrChbGq9PFgt8qouncz"
[[test.validator.clone]] # sb devnet attestation State
address = "BzqtGXZPiDSinP4xMFgPf6FLgSa6iPufK4m4JJFgMnTK"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
[package]
name = "basic_oracle"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "basic_oracle"
path = "src/lib.rs"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
switchboard-solana = { version = "=0.9.1" }
# switchboard-solana = { version = "0.9.1", path = "../../../rust/switchboard-solana" }
bytemuck = "^1"
anchor-lang = { version = "0.28.0", features = [
"init-if-needed",
"allow-missing-optionals"
] }

View File

@ -0,0 +1,37 @@
<div align="center">
![Switchboard Logo](https://github.com/switchboard-xyz/sbv2-core/raw/main/website/static/img/icons/switchboard/avatar.png)
# anchor-vrf-lite-parser
> An example program written in Anchor demonstrating how to integrate Switchboard Functions and verify attestation on-chain.
[![Anchor Test Status](https://github.com/switchboard-xyz/sbv2-solana/actions/workflows/anchor-test.yml/badge.svg)](https://github.com/switchboard-xyz/sbv2-solana/actions/workflows/anchor-test.yml)
</div>
<!-- install -->
<!-- installstop -->
## Usage
Build the example program
```bash
anchor build
```
Get your program ID and update `Anchor.toml` and `src/lib.rs` with your pubkey
```bash
export ANCHOR_VRF_LITE_PARSER_PUBKEY=$(solana-keygen pubkey target/deploy/anchor_vrf_lite_parser-keypair.json)
sed -i '' s/5Hhm5xKDiThfidbpqjJpKmMJEcKmjj5tEUNFpi2DzSvb/"$ANCHOR_VRF_LITE_PARSER_PUBKEY"/g Anchor.toml
sed -i '' s/5Hhm5xKDiThfidbpqjJpKmMJEcKmjj5tEUNFpi2DzSvb/"$ANCHOR_VRF_LITE_PARSER_PUBKEY"/g src/lib.rs
```
Then run Anchor test
```bash
anchor test
```

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,30 @@
{
"name": "solana-liquidity-oracle",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/switchboard-xyz/sbv2-solana",
"directory": "examples/functions/01_basic_oracle"
},
"scripts": {
"build:anchor": "anchor build",
"clean": "pnpm exec rimraf node_modules .anchor lib .turbo"
},
"dependencies": {
"@coral-xyz/anchor": "^0.28.0",
"@solana/spl-token": "^0.3.6",
"@solana/web3.js": "^1.78.0",
"@switchboard-xyz/common": "latest",
"@switchboard-xyz/oracle": "latest",
"@switchboard-xyz/solana.js": "latest"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^20.4.0",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^10.0.0"
}
}

View File

@ -0,0 +1,33 @@
#!/usr/bin/env tsx
import * as anchor from "@coral-xyz/anchor";
import { BasicOracle } from "../target/types/basic_oracle";
async function main() {
const program = anchor.workspace.BasicOracle as anchor.Program<BasicOracle>;
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
const getStringArg = (arg: string): string => {
const args = process.argv.slice(2);
const argIdx = args.findIndex((v) => v === arg || v === `--${arg}`);
if (argIdx === -1) {
return "";
}
if (argIdx + 1 > args.length) {
throw new Error(`Failed to find arg`);
}
return args[argIdx + 1];
};
const getFlag = (arg: string): boolean => {
const args = process.argv.slice(2);
const argIdx = args.findIndex((v) => v === arg || v === `--${arg}`);
if (argIdx === -1) {
return false;
}
return true;
};

View File

@ -0,0 +1,52 @@
import * as anchor from "@coral-xyz/anchor";
import { BasicOracle } from "../target/types/basic_oracle";
import {
Connection,
PublicKey,
Keypair,
sendAndConfirmRawTransaction,
SystemProgram,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
(async function main() {
const walletKeypair = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(require("fs").readFileSync("<YOUR_KEYPAIR_FILE>", "utf8"))
)
);
const programId = new PublicKey(
"EF68PJkRqQu2VthTSy19kg6TWynMtRmLpxcMDKEdLC8t"
);
const commitment = "processed";
const connection = new Connection("https://api.devnet.solana.com", {
commitment,
});
const wallet = new anchor.Wallet(walletKeypair);
const provider = new anchor.AnchorProvider(connection, wallet, {
commitment,
preflightCommitment: commitment,
});
const idl = await anchor.Program.fetchIdl(programId, provider);
const program = new anchor.Program(idl!, programId!, provider!);
const [state] = PublicKey.findProgramAddressSync(
[Buffer.from("BASICORACLE")],
program.programId
);
const [oracle] = PublicKey.findProgramAddressSync(
[Buffer.from("ORACLE_V1_SEED")],
program.programId
);
const initSig = await program.methods
.initialize({})
.accounts({
program: state,
oracle,
authority: walletKeypair.publicKey,
payer: walletKeypair.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([walletKeypair])
.rpc();
console.log(initSig);
})();

View File

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Added by cargo
/target
*measurement.txt

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
[package]
name = "basic-oracle-function"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "basic-oracle-function"
path = "src/main.rs"
[dependencies]
basic_oracle = { path = "../", features = ["no-entrypoint"] }
tokio = "^1"
futures = "0.3"
serde = "^1"
serde_json = "^1"
switchboard-utils = { version = "0.8.0" }
switchboard-solana = { version = "=0.9.1" }
bytemuck = "1.13.1"
rust_decimal = "1.30.0"

View File

@ -0,0 +1,31 @@
# syntax=docker/dockerfile:1.4
FROM switchboardlabs/sgx-function:main AS builder
ARG CARGO_NAME=switchboard-function
ENV CARGO_NAME=$CARGO_NAME
WORKDIR /home/root/switchboard-function
COPY ./Anchor.toml ./Cargo.lock ./Cargo.toml ./
COPY ./src ./src
WORKDIR /home/root/switchboard-function/sgx-function
COPY ./sgx-function/Cargo.lock ./sgx-function/Cargo.toml ./
COPY ./sgx-function/src ./src
RUN --mount=target=/home/root/.cargo/git,type=cache \
--mount=target=/home/root/.cargo/registry,type=cache \
--mount=type=cache,target=/home/root/switchboard-function/sgx-function/target \
cargo build --release && \
cargo strip && \
mv target/release/basic-oracle-function /sgx/app
FROM switchboardlabs/sgx-function:main
# Copy the binary
WORKDIR /sgx
COPY --from=builder /sgx/app /sgx
# Get the measurement from the enclave
RUN /get_measurement.sh
ENTRYPOINT ["bash", "/boot.sh"]

View File

@ -0,0 +1,44 @@
# syntax=docker/dockerfile:1.4
FROM switchboardlabs/sgx-function AS builder
ARG CARGO_NAME=switchboard-function
WORKDIR /home/root/solana-sdk
COPY ./rust/switchboard-solana/Cargo.toml \
./rust/switchboard-solana/Cargo.lock \
./rust/switchboard-solana/
COPY ./examples/functions/01_basic_oracle/Cargo.toml \
./examples/functions/01_basic_oracle/Cargo.lock \
./examples/functions/01_basic_oracle/
COPY ./examples/functions/01_basic_oracle/sgx-function/Cargo.toml \
./examples/functions/01_basic_oracle/sgx-function/Cargo.lock \
./examples/functions/01_basic_oracle/sgx-function/
COPY ./rust/switchboard-solana/src \
./rust/switchboard-solana/src/
COPY ./examples/functions/01_basic_oracle/src \
./examples/functions/01_basic_oracle/src/
COPY ./examples/functions/01_basic_oracle/sgx-function/src \
./examples/functions/01_basic_oracle/sgx-function/src/
WORKDIR /home/root/solana-sdk/examples/functions/01_basic_oracle/sgx-function
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=${TARGETPLATFORM} --mount=type=cache,target=target,id=${TARGETPLATFORM} \
cargo build --release && \
cargo strip && \
mv /home/root/solana-sdk/examples/functions/01_basic_oracle/sgx-function/target/release/${CARGO_NAME} /sgx
FROM switchboardlabs/sgx-function
# Copy the binary
WORKDIR /sgx
COPY --from=builder /sgx/${CARGO_NAME} /sgx/app
# Get the measurement from the enclave
RUN /get_measurement.sh
ENTRYPOINT ["bash", "/boot.sh"]

View File

@ -0,0 +1,46 @@
.PHONY: build clean publish
# Variables
CARGO_NAME=basic-oracle-function
DOCKER_IMAGE_NAME=gallynaut/binance-oracle
DOCKER_BUILD_COMMAND=DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 --build-arg CARGO_NAME=${CARGO_NAME}
# Default make task
all: build
docker_build:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile -t ${DOCKER_IMAGE_NAME}:dev --load ../
docker_publish:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile -t ${DOCKER_IMAGE_NAME} --push ../
dev_docker_build:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME}:dev --load ../../../../
dev_docker_publish:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME} --push ../../../../
build: docker_build measurement
dev: dev_docker_build measurement
publish: docker_publish measurement
latest_mr_enclave:
@docker run -d --pull=always --platform=linux/amd64 --name=latest-my-switchboard-function ${DOCKER_IMAGE_NAME}:latest > /dev/null
@docker cp latest-my-switchboard-function:/measurement.txt latest-measurement.txt
@docker stop latest-my-switchboard-function > /dev/null
@docker rm latest-my-switchboard-function > /dev/null
echo "latest MrEnclave: $(cat measurement.txt)"
measurement:
@docker run -d --platform=linux/amd64 --name=my-switchboard-function ${DOCKER_IMAGE_NAME}:dev > /dev/null
@docker cp my-switchboard-function:/measurement.txt measurement.txt
@docker stop my-switchboard-function > /dev/null
@docker rm my-switchboard-function > /dev/null
simulate: docker_build
docker run -it --platform=linux/amd64 --entrypoint=/bin/bash ${DOCKER_IMAGE_NAME}:dev /boot.sh --test
# Task to clean up the compiled rust application
clean:
cargo clean

View File

@ -0,0 +1,9 @@
{
"name": "solana-liquidity-oracle-function",
"scripts": {
"build:function": "make"
},
"dependencies": {
"solana-liquidity-oracle": "latest"
}
}

View File

@ -0,0 +1,40 @@
// Note: Binance API requires a non-US IP address
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct BinanceBook {
pub bids: Vec<(String, String)>,
pub asks: Vec<(String, String)>,
}
impl Into<NormalizedBook> for BinanceBook {
fn into(self) -> NormalizedBook {
let book = self;
let mut res = NormalizedBook::default();
for bid in book.bids.iter() {
res.bids.push(NormalizedOrdersRow {
price: Decimal::try_from(bid.0.as_str()).unwrap(),
amount: Decimal::try_from(bid.1.as_str()).unwrap(),
});
}
for ask in book.asks.iter() {
res.asks.push(NormalizedOrdersRow {
price: Decimal::try_from(ask.0.as_str()).unwrap(),
amount: Decimal::try_from(ask.1.as_str()).unwrap(),
});
}
res.price = res.bids[0]
.price
.checked_add(res.asks[0].price)
.unwrap()
.checked_div(2.into())
.unwrap();
res
}
}

View File

@ -0,0 +1,47 @@
// Note: Binance API requires a non-US IP address
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct BitfinexOrdersRow {
price: String,
amount: String,
timestamp: String,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct BitfinexBook {
pub bids: Vec<BitfinexOrdersRow>,
pub asks: Vec<BitfinexOrdersRow>,
}
impl Into<NormalizedBook> for BitfinexBook {
fn into(self) -> NormalizedBook {
let book = self;
let mut res = NormalizedBook::default();
for bid in book.bids.iter() {
res.bids.push(NormalizedOrdersRow {
price: Decimal::try_from(bid.price.as_str()).unwrap(),
amount: Decimal::try_from(bid.amount.as_str()).unwrap(),
});
}
for ask in book.asks.iter() {
res.asks.push(NormalizedOrdersRow {
price: Decimal::try_from(ask.price.as_str()).unwrap(),
amount: Decimal::try_from(ask.amount.as_str()).unwrap(),
});
}
res.price = res.bids[0]
.price
.checked_add(res.asks[0].price)
.unwrap()
.checked_div(2.into())
.unwrap();
res
}
}

View File

@ -0,0 +1,40 @@
// Note: Binance API requires a non-US IP address
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct CoinbaseBook {
pub bids: Vec<(String, String, i64)>,
pub asks: Vec<(String, String, i64)>,
}
impl Into<NormalizedBook> for CoinbaseBook {
fn into(self) -> NormalizedBook {
let book = self;
let mut res = NormalizedBook::default();
for bid in book.bids.iter() {
res.bids.push(NormalizedOrdersRow {
price: Decimal::try_from(bid.0.as_str()).unwrap(),
amount: Decimal::try_from(bid.1.as_str()).unwrap(),
});
}
for ask in book.asks.iter() {
res.asks.push(NormalizedOrdersRow {
price: Decimal::try_from(ask.0.as_str()).unwrap(),
amount: Decimal::try_from(ask.1.as_str()).unwrap(),
});
}
res.price = res.bids[0]
.price
.checked_add(res.asks[0].price)
.unwrap()
.checked_div(2.into())
.unwrap();
res
}
}

View File

@ -0,0 +1,45 @@
// Note: Binance API requires a non-US IP address
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct KrakenBookInternal {
pub bids: Vec<(String, String, i64)>,
pub asks: Vec<(String, String, i64)>,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct KrakenBook {
pub result: HashMap<String, KrakenBookInternal>,
}
impl Into<NormalizedBook> for KrakenBook {
fn into(self) -> NormalizedBook {
let book = self.result.values().next().unwrap();
let mut res = NormalizedBook::default();
for bid in book.bids.iter() {
res.bids.push(NormalizedOrdersRow {
price: Decimal::try_from(bid.0.as_str()).unwrap(),
amount: Decimal::try_from(bid.1.as_str()).unwrap(),
});
}
for ask in book.asks.iter() {
res.asks.push(NormalizedOrdersRow {
price: Decimal::try_from(ask.0.as_str()).unwrap(),
amount: Decimal::try_from(ask.1.as_str()).unwrap(),
});
}
res.price = res.bids[0]
.price
.checked_add(res.asks[0].price)
.unwrap()
.checked_div(2.into())
.unwrap();
res
}
}

View File

@ -0,0 +1,79 @@
pub mod coinbase;
pub use coinbase::*;
pub mod binance;
pub use binance::*;
pub mod bitfinex;
pub use bitfinex::*;
pub mod kraken;
pub use kraken::*;
pub use switchboard_solana::prelude::*;
use rust_decimal::Decimal;
use serde::Deserialize;
use std::collections::HashMap;
pub use switchboard_utils::reqwest;
pub use basic_oracle::{
self, OracleData, OracleDataWithTradingSymbol, RefreshPrices, RefreshPricesParams,
SwitchboardDecimal, TradingSymbol, ID as PROGRAM_ID,
};
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct NormalizedOrdersRow {
price: Decimal,
amount: Decimal,
}
#[allow(non_snake_case)]
#[derive(Deserialize, Default, Clone, Debug)]
pub struct NormalizedBook {
pub bids: Vec<NormalizedOrdersRow>,
pub asks: Vec<NormalizedOrdersRow>,
pub price: Decimal,
}
#[tokio::main(worker_threads = 12)]
async fn main() {
// First, initialize the runner instance with a freshly generated Gramine keypair
let _runner: FunctionRunner = FunctionRunner::new_from_cluster(Cluster::Devnet, None).unwrap();
let _binance_book: NormalizedBook =
reqwest::get("https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=1000")
.await
.unwrap()
.json::<BinanceBook>()
.await
.unwrap()
.into();
let _coinbase_book: NormalizedBook =
reqwest::get("https://api.pro.coinbase.com/products/BTC-USD/book?level=2")
.await
.unwrap()
.json::<CoinbaseBook>()
.await
.unwrap()
.into();
let _kraken_book: NormalizedBook =
reqwest::get("https://api.kraken.com/0/public/Depth?pair=BTCUSD")
.await
.unwrap()
.json::<KrakenBook>()
.await
.unwrap()
.into();
let _bitfinex_book: NormalizedBook = reqwest::get("https://api.bitfinex.com/v1/book/btcusd")
.await
.unwrap()
.json::<BitfinexBook>()
.await
.unwrap()
.into();
// let ixs: Vec<Instruction> = vec![];
// // Finally, emit the signed quote and partially signed transaction to the functionRunner oracle
// // The functionRunner oracle will use the last outputted word to stdout as the serialized result. This is what gets executed on-chain.
// runner.emit(ixs).await.unwrap();
}

View File

@ -0,0 +1,53 @@
use crate::*;
#[derive(Accounts)]
#[instruction(params: InitializeParams)] // rpc parameters hint
pub struct Initialize<'info> {
#[account(
init,
space = 8 + std::mem::size_of::<MyProgramState>(),
payer = payer,
seeds = [PROGRAM_SEED],
bump
)]
pub program: AccountLoader<'info, MyProgramState>,
#[account(
init,
space = 8 + std::mem::size_of::<MyOracleState>(),
payer = payer,
seeds = [ORACLE_SEED],
bump
)]
pub oracle: AccountLoader<'info, MyOracleState>,
pub authority: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct InitializeParams {}
impl Initialize<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &InitializeParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &InitializeParams) -> anchor_lang::Result<()> {
let program = &mut ctx.accounts.program.load_init()?;
program.bump = *ctx.bumps.get("program").unwrap();
program.authority = ctx.accounts.authority.key();
let oracle = &mut ctx.accounts.oracle.load_init()?;
oracle.bump = *ctx.bumps.get("oracle").unwrap();
Ok(())
}
}

View File

@ -0,0 +1,11 @@
pub mod initialize;
pub use initialize::*;
pub mod refresh_prices;
pub use refresh_prices::*;
pub mod set_function;
pub use set_function::*;
pub mod trigger_function;
pub use trigger_function::*;

View File

@ -0,0 +1,48 @@
use crate::*;
#[derive(Accounts)]
pub struct RefreshPrices<'info> {
#[account(
mut,
seeds = [ORACLE_SEED],
bump = oracle.load()?.bump
)]
pub oracle: AccountLoader<'info, MyOracleState>,
// We use this to verify the functions enclave state
#[account(
constraint =
function.load()?.validate(
&enclave_signer.to_account_info()
)? @ BasicOracleError::FunctionValidationFailed
// FunctionAccountData::validate(
// &function.to_account_info(),
// &enclave_signer.to_account_info()
// )? @ BasicOracleError::FunctionValidationFailed
)]
pub function: AccountLoader<'info, FunctionAccountData>,
pub enclave_signer: Signer<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct RefreshPricesParams {
pub rows: Vec<OracleDataWithTradingSymbol>,
}
impl RefreshPrices<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &RefreshPricesParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, params: &RefreshPricesParams) -> anchor_lang::Result<()> {
let oracle = &mut ctx.accounts.oracle.load_mut()?;
msg!("saving oracle data");
oracle.save_rows(&params.rows)?;
Ok(())
}
}

View File

@ -0,0 +1,37 @@
use crate::*;
#[derive(Accounts)]
#[instruction(params: SetFunctionParams)] // rpc parameters hint
pub struct SetFunction<'info> {
#[account(
mut,
seeds = [PROGRAM_SEED],
bump = program.load()?.bump,
has_one = authority
)]
pub program: AccountLoader<'info, MyProgramState>,
pub function: AccountLoader<'info, FunctionAccountData>,
/// CHECK:
pub authority: Signer<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct SetFunctionParams { }
impl SetFunction<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &SetFunctionParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &SetFunctionParams) -> anchor_lang::Result<()> {
let program = &mut ctx.accounts.program.load_init()?;
program.function = ctx.accounts.function.key();
Ok(())
}
}

View File

@ -0,0 +1,43 @@
use crate::*;
use switchboard_solana::attestation_program::instructions::function_trigger::FunctionTrigger;
use switchboard_solana::SWITCHBOARD_ATTESTATION_PROGRAM_ID;
#[derive(Accounts)]
#[instruction(params: TriggerFunctionParams)] // rpc parameters hint
pub struct TriggerFunction<'info> {
#[account(
has_one = authority,
has_one = attestation_queue,
)]
pub function: AccountLoader<'info, FunctionAccountData>,
pub attestation_queue: AccountLoader<'info, AttestationQueueAccountData>,
pub authority: Signer<'info>,
/// CHECK: address is explicit
#[account(address = SWITCHBOARD_ATTESTATION_PROGRAM_ID)]
pub attestation_program: AccountInfo<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct TriggerFunctionParams { }
impl TriggerFunction<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &TriggerFunctionParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &TriggerFunctionParams) -> anchor_lang::Result<()> {
FunctionTrigger {
function: ctx.accounts.function.clone(),
authority: ctx.accounts.authority.clone(),
attestation_queue: ctx.accounts.attestation_queue.clone(),
}.invoke(ctx.accounts.attestation_program.clone())?;
Ok(())
}
}

View File

@ -0,0 +1,24 @@
use crate::*;
#[error_code]
#[derive(Eq, PartialEq)]
pub enum BasicOracleError {
#[msg("Invalid authority account")]
InvalidAuthority,
#[msg("Array overflow")]
ArrayOverflow,
#[msg("Stale data")]
StaleData,
#[msg("Invalid trusted signer")]
InvalidTrustedSigner,
#[msg("Invalid MRENCLAVE")]
InvalidMrEnclave,
#[msg("Failed to find a valid trading symbol for this price")]
InvalidSymbol,
#[msg("FunctionAccount pubkey did not match program_state.function")]
IncorrectSwitchboardFunction,
#[msg("FunctionAccount pubkey did not match program_state.function")]
InvalidSwitchboardFunction,
#[msg("FunctionAccount was not validated successfully")]
FunctionValidationFailed,
}

View File

@ -0,0 +1,57 @@
pub use switchboard_solana::prelude::*;
pub mod actions;
pub use actions::*;
pub mod error;
pub use error::*;
pub mod model;
pub use model::*;
pub mod utils;
pub use utils::*;
// IDL 51F8RoK1RcduTxD8KsFGn4LUuHFnPTCf2PdAF5qEYoMU
declare_id!("BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX");
pub const PROGRAM_SEED: &[u8] = b"BASICORACLE";
pub const ORACLE_SEED: &[u8] = b"ORACLE_V1_SEED";
#[program]
pub mod basic_oracle {
use super::*;
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn initialize(
ctx: Context<Initialize>,
params: InitializeParams,
) -> anchor_lang::Result<()> {
Initialize::actuate(&ctx, &params)
}
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn refresh_oracles(
ctx: Context<RefreshPrices>,
params: RefreshPricesParams,
) -> anchor_lang::Result<()> {
RefreshPrices::actuate(&ctx, &params)
}
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn set_function(
ctx: Context<SetFunction>,
params: SetFunctionParams,
) -> anchor_lang::Result<()> {
SetFunction::actuate(&ctx, &params)
}
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn trigger_function(
ctx: Context<TriggerFunction>,
params: TriggerFunctionParams,
) -> anchor_lang::Result<()> {
TriggerFunction::actuate(&ctx, &params)
}
}

View File

@ -0,0 +1,155 @@
use crate::*;
use bytemuck::{Pod, Zeroable};
#[account(zero_copy(unsafe))]
pub struct MyProgramState {
pub bump: u8,
pub authority: Pubkey,
pub function: Pubkey,
}
#[repr(packed)]
#[zero_copy(unsafe)]
pub struct OracleData {
pub oracle_timestamp: i64,
pub price: i128,
pub volume_1hr: i128,
pub volume_24hr: i128,
pub twap_1hr: i128,
pub twap_24hr: i128,
}
#[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)]
pub struct OracleDataBorsh {
pub oracle_timestamp: i64,
pub price: i128,
pub volume_1hr: i128,
pub volume_24hr: i128,
pub twap_1hr: i128,
pub twap_24hr: i128,
}
impl From<OracleDataBorsh> for OracleData {
fn from(value: OracleDataBorsh) -> Self {
Self {
oracle_timestamp: value.oracle_timestamp,
price: value.price.clone(),
volume_1hr: value.volume_1hr.clone(),
volume_24hr: value.volume_24hr.clone(),
twap_1hr: value.twap_1hr.clone(),
twap_24hr: value.twap_24hr.clone(),
}
}
}
#[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)]
pub struct OracleDataWithTradingSymbol {
pub symbol: TradingSymbol,
pub data: OracleDataBorsh,
}
impl OracleData {
pub fn get_fair_price(&self) -> anchor_lang::Result<f64> {
// Check the price was updated in the last 10 seconds
// Do some logic here based on the twap
let price: f64 = SwitchboardDecimal {
mantissa: self.price,
scale: 9,
}
.try_into()?;
Ok(price)
}
}
#[repr(packed)]
#[account(zero_copy(unsafe))]
pub struct MyOracleState {
pub bump: u8,
pub btc: OracleData,
pub usdc: OracleData,
pub eth: OracleData,
pub sol: OracleData,
pub doge: OracleData,
// can always re-allocate to add more
// pub reserved: [u8; 2400],
}
impl MyOracleState {
pub fn save_rows(
&mut self,
rows: &Vec<OracleDataWithTradingSymbol>,
) -> anchor_lang::Result<()> {
for row in rows.iter() {
match row.symbol {
TradingSymbol::Btc => {
msg!("saving BTC price, {}", { row.data.price });
self.btc = row.data.into();
}
TradingSymbol::Usdc => {
msg!("saving USDC price, {}", { row.data.price });
self.usdc = row.data.into();
}
TradingSymbol::Eth => {
msg!("saving ETH price, {}", { row.data.price });
self.eth = row.data.into();
}
TradingSymbol::Sol => {
msg!("saving SOL price, {}", { row.data.price });
self.sol = row.data.into();
}
TradingSymbol::Doge => {
msg!("saving DOGE price, {}", { row.data.price });
self.doge = row.data.into();
}
_ => {
msg!("no trading symbol found for {:?}", row.symbol);
// TODO: emit an event so we can detect and fix
}
}
}
Ok(())
}
}
#[repr(u8)]
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, AnchorSerialize, AnchorDeserialize)]
pub enum TradingSymbol {
#[default]
Unknown = 0,
Btc = 1,
Usdc = 2,
Eth = 3,
Sol = 4,
Doge = 5,
}
unsafe impl Pod for TradingSymbol {}
unsafe impl Zeroable for TradingSymbol {}
impl From<TradingSymbol> for u8 {
fn from(value: TradingSymbol) -> Self {
match value {
TradingSymbol::Btc => 1,
TradingSymbol::Usdc => 2,
TradingSymbol::Eth => 3,
TradingSymbol::Sol => 4,
TradingSymbol::Doge => 5,
_ => 0,
}
}
}
impl From<u8> for TradingSymbol {
fn from(value: u8) -> Self {
match value {
1 => TradingSymbol::Btc,
2 => TradingSymbol::Usdc,
3 => TradingSymbol::Eth,
4 => TradingSymbol::Sol,
5 => TradingSymbol::Doge,
_ => TradingSymbol::default(),
}
}
}

View File

@ -0,0 +1,48 @@
pub use crate::*;
pub fn parse_mr_enclaves(enclaves: &Vec<[u8; 32]>) -> anchor_lang::Result<[[u8; 32]; 32]> {
// enclaves
// .clone()
// .try_into()
// .map_err(|_| error!(BasicOracleError::ArrayOverflow))
if enclaves.len() > 32 {
return Err(error!(BasicOracleError::ArrayOverflow));
}
let mut result: [[u8; 32]; 32] = [[0; 32]; 32];
for (i, enclave) in enclaves.iter().enumerate() {
result[i] = *enclave;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mr_enclaves_success() {
let enclaves: Vec<[u8; 32]> = vec![[1; 32]; 10];
let result = parse_mr_enclaves(&enclaves).unwrap();
// Check first 10 elements are [1; 32]
for i in 0..10 {
assert_eq!(result[i], [1; 32]);
}
// Check the remaining elements are [0; 32] (default)
for i in 10..32 {
assert_eq!(result[i], [0; 32]);
}
}
// #[test]
// fn test_parse_mr_enclaves_overflow() {
// let enclaves: Vec<[u8; 32]> = vec![[1; 32]; 33];
// match parse_mr_enclaves(&enclaves) {
// Err(BasicOracleError::ArrayOverflow) => {} // test passes
// _ => panic!("Unexpected result"), // test fails
// };
// }
}

View File

@ -0,0 +1,246 @@
// eslint-disable-next-line node/no-unpublished-import
import type { BasicOracle } from "../target/types/basic_oracle";
import { printLogs } from "./utils";
import type { Program } from "@coral-xyz/anchor";
import * as anchor from "@coral-xyz/anchor";
import { sleep } from "@switchboard-xyz/common";
import type { FunctionAccount, MrEnclave } from "@switchboard-xyz/solana.js";
import { SwitchboardWallet } from "@switchboard-xyz/solana.js";
import {
AttestationProgramStateAccount,
AttestationQueueAccount,
attestationTypes,
type BootstrappedAttestationQueue,
parseMrEnclave,
SwitchboardProgram,
types,
} from "@switchboard-xyz/solana.js";
const unixTimestamp = () => Math.floor(Date.now() / 1000);
// vv1gTnfuUiroqgJHS4xsRASsRQqqixCv1su85VWvcP9
const MRENCLAVE = parseMrEnclave(
Buffer.from("Y6keo0uTCiWDNcWwGjZ2jfTd4VFhrr6LC/6Mk1aiNCA=", "base64")
);
const emptyEnclave: number[] = new Array(32).fill(0);
function has_mr_enclave(
enclaves: Array<MrEnclave>,
unknown_enclave: MrEnclave
) {
return enclaves.includes(unknown_enclave);
}
describe("basic_oracle", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicOracle as Program<BasicOracle>;
const payer = (program.provider as anchor.AnchorProvider).publicKey;
const programStatePubkey = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("BASICORACLE")],
program.programId
)[0];
const oraclePubkey = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("ORACLE_V1_SEED")],
program.programId
)[0];
let switchboard: BootstrappedAttestationQueue;
let wallet: SwitchboardWallet;
let functionAccount: FunctionAccount;
before(async () => {
const switchboardProgram = await SwitchboardProgram.fromProvider(
program.provider as anchor.AnchorProvider
);
await AttestationProgramStateAccount.getOrCreate(switchboardProgram);
switchboard = await AttestationQueueAccount.bootstrapNewQueue(
switchboardProgram
);
console.log(`programStatePubkey: ${programStatePubkey}`);
[wallet] = await SwitchboardWallet.create(
switchboard.program,
switchboard.attestationQueue.publicKey,
payer,
"MySharedWallet",
16
);
console.log(`wallet: ${wallet.publicKey}`);
[functionAccount] =
await switchboard.attestationQueue.account.createFunction(
{
name: "test function",
metadata: "this function handles XYZ for my protocol",
schedule: "15 * * * * *",
container: "switchboardlabs/basic-oracle-function",
version: "latest",
mrEnclave: MRENCLAVE,
authority: programStatePubkey,
},
wallet
);
console.log(`functionAccount: ${functionAccount.publicKey}`);
});
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize({})
.accounts({
program: programStatePubkey,
oracle: oraclePubkey,
authority: payer,
payer: payer,
// function: functionAccount.publicKey,
})
.rpc()
.catch((err) => {
console.error(err);
throw err;
});
console.log("Your transaction signature", tx);
});
// it("Adds an enclave measurement", async () => {
// // Add your test here.
// const tx = await program.methods
// .setEnclaves({ mrEnclaves: [Array.from(MRENCLAVE)] })
// .accounts({
// program: programStatePubkey,
// authority: payer,
// })
// .rpc()
// .catch((err) => {
// console.error(err);
// throw err;
// });
// console.log("Your transaction signature", tx);
// const programState = await program.account.myProgramState.fetch(
// programStatePubkey
// );
// });
it("Oracle refreshes the prices", async () => {
const securedSigner = anchor.web3.Keypair.generate();
const rewardAddress =
await switchboard.program.mint.getOrCreateAssociatedUser(payer);
const functionState = await functionAccount.loadData();
// TODO: generate function verify ixn
const functionVerifyIxn = attestationTypes.functionVerify(
switchboard.program,
{
params: {
observedTime: new anchor.BN(unixTimestamp()),
nextAllowedTimestamp: new anchor.BN(unixTimestamp() + 100),
isFailure: false,
mrEnclave: Array.from(MRENCLAVE),
},
},
{
function: functionAccount.publicKey,
functionEnclaveSigner: securedSigner.publicKey,
verifier: switchboard.verifier.publicKey,
verifierSigner: switchboard.verifier.signer.publicKey,
attestationQueue: switchboard.attestationQueue.publicKey,
escrowWallet: functionState.escrowWallet,
escrowTokenWallet: functionState.escrowTokenWallet,
receiver: rewardAddress,
verifierPermission: switchboard.verifier.permissionAccount.publicKey,
tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
}
);
// Add your test here.
const tx = await program.methods
.refreshOracles({
rows: [
{
symbol: { btc: {} },
data: {
oracleTimestamp: new anchor.BN(unixTimestamp()),
price: new anchor.BN("25225000000000"), // 25225
volume1hr: new anchor.BN("25225000000000"), // 1337000
volume24hr: new anchor.BN("25225000000000"), // 1337000
twap1hr: new anchor.BN("25225000000000"), // 25550
twap24hr: new anchor.BN("25225000000000"), // 25550
},
},
{
symbol: { eth: {} },
data: {
oracleTimestamp: new anchor.BN(unixTimestamp()),
price: new anchor.BN("1750000000000"), // 1750
volume1hr: new anchor.BN("420000000000"), // 420000
volume24hr: new anchor.BN("420000000000"), // 420000
twap1hr: new anchor.BN("1750000000000"), // 1750
twap24hr: new anchor.BN("1750000000000"), // 1750
},
},
],
})
.accounts({
oracle: oraclePubkey,
function: functionAccount.publicKey,
enclaveSigner: securedSigner.publicKey,
})
.preInstructions([functionVerifyIxn])
.signers([switchboard.verifier.signer, securedSigner])
.rpc({ skipPreflight: true });
console.log("Your transaction signature", tx);
await printLogs(switchboard.program.connection, tx ? tx : "");
await sleep(5000);
const oracleState = await program.account.myOracleState.fetch(oraclePubkey);
console.log(oracleState);
console.log(`BTC\n`);
printData(oracleState.btc);
console.log(`ETH\n`);
printData(oracleState.eth);
console.log(`SOL\n`);
printData(oracleState.sol);
});
});
function normalizeDecimals(value: anchor.BN) {
return (value ?? new anchor.BN(0))
.div(new anchor.BN(10).pow(new anchor.BN(9)))
.toNumber();
}
function printData(obj: {
oracleTimestamp: anchor.BN;
price: anchor.BN;
volume1hr: anchor.BN;
volume24hr: anchor.BN;
twap1hr: anchor.BN;
twap24hr: anchor.BN;
}) {
console.log(`\tprice: ${normalizeDecimals(obj.price)}`);
console.log(`\ttimestamp: ${obj.oracleTimestamp.toNumber()}`);
console.log(`\t1Hr Volume: ${normalizeDecimals(obj.volume1hr)}`);
console.log(`\t24Hr Volume: ${normalizeDecimals(obj.volume24hr)}`);
console.log(`\t1Hr Twap: ${normalizeDecimals(obj.twap1hr)}`);
console.log(`\t24Hr Twap: ${normalizeDecimals(obj.twap24hr)}`);
}

View File

@ -0,0 +1,16 @@
import type { Connection } from "@solana/web3.js";
import { sleep } from "@switchboard-xyz/common";
export async function printLogs(
connection: Connection,
tx: string,
v0Txn?: boolean,
delay = 3000
) {
await sleep(delay);
const parsed = await connection.getParsedTransaction(tx, {
commitment: "confirmed",
maxSupportedTransactionVersion: v0Txn ? 0 : undefined,
});
console.log(parsed?.meta?.logMessages?.join("\n"));
}

View File

@ -0,0 +1,27 @@
{
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
},
"compilerOptions": {
"types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"],
"module": "commonjs",
"noEmit": true,
"esModuleInterop": true,
"strict": false,
"strictNullChecks": false,
"target": "es6",
"paths": {
"@switchboard-xyz/solana.js": ["../../../javascript/solana.js"]
}
},
"include": ["tests/**/*", "target/types/*.ts"],
"exclude": ["target", "lib"],
"references": [
{
"path": "../../../../../javascript/solana.js"
}
]
}

View File

@ -0,0 +1,10 @@
target/
Makefile
README.md
node_modules
.turbo
measurement.txt
.anchor
scripts
.prettierignore
.gitignore

View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
**/*.rs.bk
node_modules
test-ledger
.yarn

View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
node_modules
dist
build
test-ledger

View File

@ -0,0 +1,49 @@
[workspace]
members = ["."]
[features]
seeds = false
skip-lint = false
[programs.localnet]
basic_oracle = "BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX"
[programs.devnet]
basic_oracle = "BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX"
[provider]
# cluster = "https://api.devnet.solana.com"
# wallet = "~/switchboard_environments_v2/devnet/upgrade_authority/upgrade_authority.json"
cluster = "Localnet"
wallet = "~/.config/solana/id.json"
# cluster = "devnet"
[scripts]
test = "pnpm exec ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
[test]
startup_wait = 15000
[test.validator]
url = "https://api.devnet.solana.com"
[[test.validator.clone]] # sb devnet oracle programID
address = "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
[[test.validator.clone]] # sb devnet oracle IDL
address = "Fi8vncGpNKbq62gPo56G4toCehWNy77GgqGkTaAF5Lkk"
[[test.validator.clone]] # sb devnet oracle SbState
address = "CyZuD7RPDcrqCGbNvLCyqk6Py9cEZTKmNKujfPi3ynDd"
[[test.validator.clone]] # sb devnet oracle tokenVault
address = "7hkp1xfPBcD2t1vZMoWWQPzipHVcXeLAAaiGXdPSfDie"
[[test.validator.clone]] # sb devnet attestation programID
address = "sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx"
[[test.validator.clone]] # sb devnet attestation IDL
address = "5ExuoQR69trmKQfB95fDsUGsUrrChbGq9PFgt8qouncz"
[[test.validator.clone]] # sb devnet attestation State
address = "BzqtGXZPiDSinP4xMFgPf6FLgSa6iPufK4m4JJFgMnTK"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
[package]
name = "basic_oracle"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "basic_oracle"
path = "src/lib.rs"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
switchboard-solana = { version = "=0.9.1" }
# switchboard-solana = { version = "0.9.1", path = "../../../rust/switchboard-solana" }
bytemuck = "^1"
anchor-lang = { version = "0.28.0", features = [
"init-if-needed",
"allow-missing-optionals"
] }

View File

@ -0,0 +1,37 @@
<div align="center">
![Switchboard Logo](https://github.com/switchboard-xyz/sbv2-core/raw/main/website/static/img/icons/switchboard/avatar.png)
# anchor-vrf-lite-parser
> An example program written in Anchor demonstrating how to integrate Switchboard Functions and verify attestation on-chain.
[![Anchor Test Status](https://github.com/switchboard-xyz/sbv2-solana/actions/workflows/anchor-test.yml/badge.svg)](https://github.com/switchboard-xyz/sbv2-solana/actions/workflows/anchor-test.yml)
</div>
<!-- install -->
<!-- installstop -->
## Usage
Build the example program
```bash
anchor build
```
Get your program ID and update `Anchor.toml` and `src/lib.rs` with your pubkey
```bash
export ANCHOR_VRF_LITE_PARSER_PUBKEY=$(solana-keygen pubkey target/deploy/anchor_vrf_lite_parser-keypair.json)
sed -i '' s/5Hhm5xKDiThfidbpqjJpKmMJEcKmjj5tEUNFpi2DzSvb/"$ANCHOR_VRF_LITE_PARSER_PUBKEY"/g Anchor.toml
sed -i '' s/5Hhm5xKDiThfidbpqjJpKmMJEcKmjj5tEUNFpi2DzSvb/"$ANCHOR_VRF_LITE_PARSER_PUBKEY"/g src/lib.rs
```
Then run Anchor test
```bash
anchor test
```

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,30 @@
{
"name": "solana-candles-oracle-example",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/switchboard-xyz/sbv2-solana",
"directory": "examples/functions/01_basic_oracle"
},
"scripts": {
"build:anchor": "anchor build",
"clean": "pnpm exec rimraf node_modules .anchor lib .turbo"
},
"dependencies": {
"@coral-xyz/anchor": "^0.28.0",
"@solana/spl-token": "^0.3.6",
"@solana/web3.js": "^1.78.0",
"@switchboard-xyz/common": "latest",
"@switchboard-xyz/oracle": "latest",
"@switchboard-xyz/solana.js": "latest"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^20.4.0",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^10.0.0"
}
}

View File

@ -0,0 +1,33 @@
#!/usr/bin/env tsx
import * as anchor from "@coral-xyz/anchor";
import { BasicOracle } from "../target/types/basic_oracle";
async function main() {
const program = anchor.workspace.BasicOracle as anchor.Program<BasicOracle>;
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
const getStringArg = (arg: string): string => {
const args = process.argv.slice(2);
const argIdx = args.findIndex((v) => v === arg || v === `--${arg}`);
if (argIdx === -1) {
return "";
}
if (argIdx + 1 > args.length) {
throw new Error(`Failed to find arg`);
}
return args[argIdx + 1];
};
const getFlag = (arg: string): boolean => {
const args = process.argv.slice(2);
const argIdx = args.findIndex((v) => v === arg || v === `--${arg}`);
if (argIdx === -1) {
return false;
}
return true;
};

View File

@ -0,0 +1,52 @@
import * as anchor from "@coral-xyz/anchor";
import { BasicOracle } from "../target/types/basic_oracle";
import {
Connection,
PublicKey,
Keypair,
sendAndConfirmRawTransaction,
SystemProgram,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
(async function main() {
const walletKeypair = Keypair.fromSecretKey(
new Uint8Array(
JSON.parse(require("fs").readFileSync("<YOUR_KEYPAIR_FILE>", "utf8"))
)
);
const programId = new PublicKey(
"EF68PJkRqQu2VthTSy19kg6TWynMtRmLpxcMDKEdLC8t"
);
const commitment = "processed";
const connection = new Connection("https://api.devnet.solana.com", {
commitment,
});
const wallet = new anchor.Wallet(walletKeypair);
const provider = new anchor.AnchorProvider(connection, wallet, {
commitment,
preflightCommitment: commitment,
});
const idl = await anchor.Program.fetchIdl(programId, provider);
const program = new anchor.Program(idl!, programId!, provider!);
const [state] = PublicKey.findProgramAddressSync(
[Buffer.from("BASICORACLE")],
program.programId
);
const [oracle] = PublicKey.findProgramAddressSync(
[Buffer.from("ORACLE_V1_SEED")],
program.programId
);
const initSig = await program.methods
.initialize({})
.accounts({
program: state,
oracle,
authority: walletKeypair.publicKey,
payer: walletKeypair.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([walletKeypair])
.rpc();
console.log(initSig);
})();

View File

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Added by cargo
/target
*measurement.txt

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
[package]
name = "basic-oracle-function"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "basic-oracle-function"
path = "src/main.rs"
[dependencies]
basic_oracle = { path = "../", features = ["no-entrypoint"] }
tokio = "^1"
futures = "0.3"
serde = "^1"
serde_json = "^1"
switchboard-utils = { version = "0.8.0" }
switchboard-solana = { version = "=0.9.1" }
bytemuck = "1.13.1"
rust_decimal = "1.30.0"

View File

@ -0,0 +1,31 @@
# syntax=docker/dockerfile:1.4
FROM switchboardlabs/sgx-function:main AS builder
ARG CARGO_NAME=switchboard-function
ENV CARGO_NAME=$CARGO_NAME
WORKDIR /home/root/switchboard-function
COPY ./Anchor.toml ./Cargo.lock ./Cargo.toml ./
COPY ./src ./src
WORKDIR /home/root/switchboard-function/sgx-function
COPY ./sgx-function/Cargo.lock ./sgx-function/Cargo.toml ./
COPY ./sgx-function/src ./src
RUN --mount=target=/home/root/.cargo/git,type=cache \
--mount=target=/home/root/.cargo/registry,type=cache \
--mount=type=cache,target=/home/root/switchboard-function/sgx-function/target \
cargo build --release && \
cargo strip && \
mv target/release/basic-oracle-function /sgx/app
FROM switchboardlabs/sgx-function:main
# Copy the binary
WORKDIR /sgx
COPY --from=builder /sgx/app /sgx
# Get the measurement from the enclave
RUN /get_measurement.sh
ENTRYPOINT ["bash", "/boot.sh"]

View File

@ -0,0 +1,44 @@
# syntax=docker/dockerfile:1.4
FROM switchboardlabs/sgx-function AS builder
ARG CARGO_NAME=switchboard-function
WORKDIR /home/root/solana-sdk
COPY ./rust/switchboard-solana/Cargo.toml \
./rust/switchboard-solana/Cargo.lock \
./rust/switchboard-solana/
COPY ./examples/functions/01_basic_oracle/Cargo.toml \
./examples/functions/01_basic_oracle/Cargo.lock \
./examples/functions/01_basic_oracle/
COPY ./examples/functions/01_basic_oracle/sgx-function/Cargo.toml \
./examples/functions/01_basic_oracle/sgx-function/Cargo.lock \
./examples/functions/01_basic_oracle/sgx-function/
COPY ./rust/switchboard-solana/src \
./rust/switchboard-solana/src/
COPY ./examples/functions/01_basic_oracle/src \
./examples/functions/01_basic_oracle/src/
COPY ./examples/functions/01_basic_oracle/sgx-function/src \
./examples/functions/01_basic_oracle/sgx-function/src/
WORKDIR /home/root/solana-sdk/examples/functions/01_basic_oracle/sgx-function
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=${TARGETPLATFORM} --mount=type=cache,target=target,id=${TARGETPLATFORM} \
cargo build --release && \
cargo strip && \
mv /home/root/solana-sdk/examples/functions/01_basic_oracle/sgx-function/target/release/${CARGO_NAME} /sgx
FROM switchboardlabs/sgx-function
# Copy the binary
WORKDIR /sgx
COPY --from=builder /sgx/${CARGO_NAME} /sgx/app
# Get the measurement from the enclave
RUN /get_measurement.sh
ENTRYPOINT ["bash", "/boot.sh"]

View File

@ -0,0 +1,46 @@
.PHONY: build clean publish
# Variables
CARGO_NAME=basic-oracle-function
DOCKER_IMAGE_NAME=gallynaut/binance-oracle
DOCKER_BUILD_COMMAND=DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 --build-arg CARGO_NAME=${CARGO_NAME}
# Default make task
all: build
docker_build:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile -t ${DOCKER_IMAGE_NAME}:dev --load ../
docker_publish:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile -t ${DOCKER_IMAGE_NAME} --push ../
dev_docker_build:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME}:dev --load ../../../../
dev_docker_publish:
${DOCKER_BUILD_COMMAND} --pull -f Dockerfile.dev -t ${DOCKER_IMAGE_NAME} --push ../../../../
build: docker_build measurement
dev: dev_docker_build measurement
publish: docker_publish measurement
latest_mr_enclave:
@docker run -d --pull=always --platform=linux/amd64 --name=latest-my-switchboard-function ${DOCKER_IMAGE_NAME}:latest > /dev/null
@docker cp latest-my-switchboard-function:/measurement.txt latest-measurement.txt
@docker stop latest-my-switchboard-function > /dev/null
@docker rm latest-my-switchboard-function > /dev/null
echo "latest MrEnclave: $(cat measurement.txt)"
measurement:
@docker run -d --platform=linux/amd64 --name=my-switchboard-function ${DOCKER_IMAGE_NAME}:dev > /dev/null
@docker cp my-switchboard-function:/measurement.txt measurement.txt
@docker stop my-switchboard-function > /dev/null
@docker rm my-switchboard-function > /dev/null
simulate: docker_build
docker run -it --platform=linux/amd64 --entrypoint=/bin/bash ${DOCKER_IMAGE_NAME}:dev /boot.sh --test
# Task to clean up the compiled rust application
clean:
cargo clean

View File

@ -0,0 +1,9 @@
{
"name": "solana-candles-oracle-function",
"scripts": {
"build:function": "make"
},
"dependencies": {
"solana-candles-oracle-example": "latest"
}
}

View File

@ -0,0 +1,35 @@
// Note: Binance API requires a non-US IP address
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[derive(Deserialize, Default, Clone, Debug)]
pub struct BinanceCandle(
i64, // Kline open time
String, // Open price
String, // High price
String, // Low price
String, // Close price
String, // Volume
i64, // Kline Close time
String, // Quote asset volume
i64, // Number of trades
String, // Taker buy base asset volume
String, // Taker buy quote asset volume
String, // - Unused -
);
impl Into<NormalizedCandle> for BinanceCandle {
fn into(self) -> NormalizedCandle {
NormalizedCandle {
open_time: self.0,
open_price: Decimal::try_from(self.1.as_str()).unwrap(),
high_price: Decimal::try_from(self.2.as_str()).unwrap(),
low_price: Decimal::try_from(self.3.as_str()).unwrap(),
close_price: Decimal::try_from(self.4.as_str()).unwrap(),
volume: Decimal::try_from(self.5.as_str()).unwrap(),
}
}
}

View File

@ -0,0 +1,27 @@
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[derive(Deserialize, Default, Clone, Debug)]
pub struct BitfinexCandle(
i64, // open time
String, // Open price
String, // High price
String, // Low price
String, // Close price
String, // Volume
);
impl Into<NormalizedCandle> for BitfinexCandle {
fn into(self) -> NormalizedCandle {
NormalizedCandle {
open_time: self.0,
open_price: Decimal::try_from(self.1.as_str()).unwrap(),
high_price: Decimal::try_from(self.2.as_str()).unwrap(),
low_price: Decimal::try_from(self.3.as_str()).unwrap(),
close_price: Decimal::try_from(self.4.as_str()).unwrap(),
volume: Decimal::try_from(self.5.as_str()).unwrap(),
}
}
}

View File

@ -0,0 +1,27 @@
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[derive(Deserialize, Default, Clone, Debug)]
pub struct CoinbaseCandle(
i64, // open time
f64, // Low price
f64, // High price
f64, // Open price
f64, // Close price
f64, // Volume
);
impl Into<NormalizedCandle> for CoinbaseCandle {
fn into(self) -> NormalizedCandle {
NormalizedCandle {
open_time: self.0,
low_price: Decimal::try_from(self.1).unwrap(),
high_price: Decimal::try_from(self.2).unwrap(),
open_price: Decimal::try_from(self.3).unwrap(),
close_price: Decimal::try_from(self.4).unwrap(),
volume: Decimal::try_from(self.5).unwrap(),
}
}
}

View File

@ -0,0 +1,39 @@
use crate::*;
pub use switchboard_utils::reqwest;
use serde::Deserialize;
#[derive(Deserialize, Default, Clone, Debug)]
pub struct KrakenCandle(
String, // Open time
String, // Low price
String, // High price
String, // Open price
String, // Close price
String, // VWAP // TODO: surface this
String, // Volume
String, // Count
);
#[derive(Deserialize, Default, Clone, Debug)]
pub struct KrakenCandleResponse {
pub result: HashMap<String, Vec<KrakenCandle>>,
}
impl Into<Vec<NormalizedCandle>> for KrakenCandleResponse {
fn into(self) -> Vec<NormalizedCandle> {
let candles = self.result.values().next().unwrap();
let mut res = vec![];
for candle in candles {
res.push(NormalizedCandle {
open_time: candle.0.parse().unwrap(),
low_price: Decimal::try_from(candle.1.as_str()).unwrap(),
high_price: Decimal::try_from(candle.2.as_str()).unwrap(),
open_price: Decimal::try_from(candle.3.as_str()).unwrap(),
close_price: Decimal::try_from(candle.4.as_str()).unwrap(),
volume: Decimal::try_from(candle.5.as_str()).unwrap(),
});
}
res
}
}

View File

@ -0,0 +1,90 @@
pub mod coinbase;
pub use coinbase::*;
pub mod binance;
pub use binance::*;
pub mod bitfinex;
pub use bitfinex::*;
pub mod kraken;
pub use kraken::*;
pub use switchboard_solana::prelude::*;
use rust_decimal::Decimal;
use serde::Deserialize;
use std::collections::HashMap;
pub use switchboard_utils::reqwest;
pub use basic_oracle::{
self, OracleData, OracleDataWithTradingSymbol, RefreshPrices, RefreshPricesParams,
SwitchboardDecimal, TradingSymbol, ID as PROGRAM_ID,
};
pub fn ix_discriminator(name: &str) -> [u8; 8] {
let preimage = format!("global:{}", name);
let mut sighash = [0u8; 8];
sighash.copy_from_slice(
&anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8],
);
sighash
}
#[derive(Deserialize, Default, Clone, Debug)]
pub struct NormalizedCandle {
pub open_time: i64,
pub open_price: Decimal,
pub high_price: Decimal,
pub low_price: Decimal,
pub close_price: Decimal,
pub volume: Decimal,
}
#[tokio::main(worker_threads = 12)]
async fn main() {
// First, initialize the runner instance with a freshly generated Gramine keypair
let _runner: FunctionRunner = FunctionRunner::new_from_cluster(Cluster::Devnet, None).unwrap();
let _binance_candles: Vec<NormalizedCandle> =
reqwest::get("https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&limit=1")
.await
.unwrap()
.json::<Vec<BinanceCandle>>()
.await
.unwrap()
.iter()
.map(|c| c.clone().into())
.collect();
let _coinbase_candles: Vec<NormalizedCandle> =
reqwest::get("https://api.pro.coinbase.com/products/BTC-USD/candles?granularity=60")
.await
.unwrap()
.json::<Vec<CoinbaseCandle>>()
.await
.unwrap()
.iter()
.map(|c| c.clone().into())
.collect();
let _kraken_candle: Vec<NormalizedCandle> =
reqwest::get("https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1")
.await
.unwrap()
.json::<KrakenCandleResponse>()
.await
.unwrap()
.into();
let _bitfinex_candle: Vec<NormalizedCandle> = reqwest::get("https://api-pub.bitfinex.com/v2/candles/trade:1m:tBTCUSD/hist?limit=1")
.await
.unwrap()
.json::<Vec<BitfinexCandle>>()
.await
.unwrap()
.iter()
.map(|c| c.clone().into())
.collect();
// let ixs: Vec<Instruction> = vec![];
// // Finally, emit the signed quote and partially signed transaction to the functionRunner oracle
// // The functionRunner oracle will use the last outputted word to stdout as the serialized result. This is what gets executed on-chain.
// runner.emit(ixs).await.unwrap();
}

View File

@ -0,0 +1,53 @@
use crate::*;
#[derive(Accounts)]
#[instruction(params: InitializeParams)] // rpc parameters hint
pub struct Initialize<'info> {
#[account(
init,
space = 8 + std::mem::size_of::<MyProgramState>(),
payer = payer,
seeds = [PROGRAM_SEED],
bump
)]
pub program: AccountLoader<'info, MyProgramState>,
#[account(
init,
space = 8 + std::mem::size_of::<MyOracleState>(),
payer = payer,
seeds = [ORACLE_SEED],
bump
)]
pub oracle: AccountLoader<'info, MyOracleState>,
pub authority: Signer<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct InitializeParams {}
impl Initialize<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &InitializeParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &InitializeParams) -> anchor_lang::Result<()> {
let program = &mut ctx.accounts.program.load_init()?;
program.bump = *ctx.bumps.get("program").unwrap();
program.authority = ctx.accounts.authority.key();
let oracle = &mut ctx.accounts.oracle.load_init()?;
oracle.bump = *ctx.bumps.get("oracle").unwrap();
Ok(())
}
}

View File

@ -0,0 +1,11 @@
pub mod initialize;
pub use initialize::*;
pub mod refresh_prices;
pub use refresh_prices::*;
pub mod set_function;
pub use set_function::*;
pub mod trigger_function;
pub use trigger_function::*;

View File

@ -0,0 +1,48 @@
use crate::*;
#[derive(Accounts)]
pub struct RefreshPrices<'info> {
#[account(
mut,
seeds = [ORACLE_SEED],
bump = oracle.load()?.bump
)]
pub oracle: AccountLoader<'info, MyOracleState>,
// We use this to verify the functions enclave state
#[account(
constraint =
function.load()?.validate(
&enclave_signer.to_account_info()
)? @ BasicOracleError::FunctionValidationFailed
// FunctionAccountData::validate(
// &function.to_account_info(),
// &enclave_signer.to_account_info()
// )? @ BasicOracleError::FunctionValidationFailed
)]
pub function: AccountLoader<'info, FunctionAccountData>,
pub enclave_signer: Signer<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct RefreshPricesParams {
pub rows: Vec<OracleDataWithTradingSymbol>,
}
impl RefreshPrices<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &RefreshPricesParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, params: &RefreshPricesParams) -> anchor_lang::Result<()> {
let oracle = &mut ctx.accounts.oracle.load_mut()?;
msg!("saving oracle data");
oracle.save_rows(&params.rows)?;
Ok(())
}
}

View File

@ -0,0 +1,37 @@
use crate::*;
#[derive(Accounts)]
#[instruction(params: SetFunctionParams)] // rpc parameters hint
pub struct SetFunction<'info> {
#[account(
mut,
seeds = [PROGRAM_SEED],
bump = program.load()?.bump,
has_one = authority
)]
pub program: AccountLoader<'info, MyProgramState>,
pub function: AccountLoader<'info, FunctionAccountData>,
/// CHECK:
pub authority: Signer<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct SetFunctionParams { }
impl SetFunction<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &SetFunctionParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &SetFunctionParams) -> anchor_lang::Result<()> {
let program = &mut ctx.accounts.program.load_init()?;
program.function = ctx.accounts.function.key();
Ok(())
}
}

View File

@ -0,0 +1,43 @@
use crate::*;
use switchboard_solana::attestation_program::instructions::function_trigger::FunctionTrigger;
use switchboard_solana::SWITCHBOARD_ATTESTATION_PROGRAM_ID;
#[derive(Accounts)]
#[instruction(params: TriggerFunctionParams)] // rpc parameters hint
pub struct TriggerFunction<'info> {
#[account(
has_one = authority,
has_one = attestation_queue,
)]
pub function: AccountLoader<'info, FunctionAccountData>,
pub attestation_queue: AccountLoader<'info, AttestationQueueAccountData>,
pub authority: Signer<'info>,
/// CHECK: address is explicit
#[account(address = SWITCHBOARD_ATTESTATION_PROGRAM_ID)]
pub attestation_program: AccountInfo<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct TriggerFunctionParams { }
impl TriggerFunction<'_> {
pub fn validate(
&self,
_ctx: &Context<Self>,
_params: &TriggerFunctionParams,
) -> anchor_lang::Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, _params: &TriggerFunctionParams) -> anchor_lang::Result<()> {
FunctionTrigger {
function: ctx.accounts.function.clone(),
authority: ctx.accounts.authority.clone(),
attestation_queue: ctx.accounts.attestation_queue.clone(),
}.invoke(ctx.accounts.attestation_program.clone())?;
Ok(())
}
}

View File

@ -0,0 +1,24 @@
use crate::*;
#[error_code]
#[derive(Eq, PartialEq)]
pub enum BasicOracleError {
#[msg("Invalid authority account")]
InvalidAuthority,
#[msg("Array overflow")]
ArrayOverflow,
#[msg("Stale data")]
StaleData,
#[msg("Invalid trusted signer")]
InvalidTrustedSigner,
#[msg("Invalid MRENCLAVE")]
InvalidMrEnclave,
#[msg("Failed to find a valid trading symbol for this price")]
InvalidSymbol,
#[msg("FunctionAccount pubkey did not match program_state.function")]
IncorrectSwitchboardFunction,
#[msg("FunctionAccount pubkey did not match program_state.function")]
InvalidSwitchboardFunction,
#[msg("FunctionAccount was not validated successfully")]
FunctionValidationFailed,
}

View File

@ -0,0 +1,57 @@
pub use switchboard_solana::prelude::*;
pub mod actions;
pub use actions::*;
pub mod error;
pub use error::*;
pub mod model;
pub use model::*;
pub mod utils;
pub use utils::*;
// IDL 51F8RoK1RcduTxD8KsFGn4LUuHFnPTCf2PdAF5qEYoMU
declare_id!("BkTMjFhosJ1xKtLMV2xchGtnTDBABLJ45aXzs7x9FdeX");
pub const PROGRAM_SEED: &[u8] = b"BASICORACLE";
pub const ORACLE_SEED: &[u8] = b"ORACLE_V1_SEED";
#[program]
pub mod basic_oracle {
use super::*;
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn initialize(
ctx: Context<Initialize>,
params: InitializeParams,
) -> anchor_lang::Result<()> {
Initialize::actuate(&ctx, &params)
}
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn refresh_oracles(
ctx: Context<RefreshPrices>,
params: RefreshPricesParams,
) -> anchor_lang::Result<()> {
RefreshPrices::actuate(&ctx, &params)
}
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn set_function(
ctx: Context<SetFunction>,
params: SetFunctionParams,
) -> anchor_lang::Result<()> {
SetFunction::actuate(&ctx, &params)
}
#[access_control(ctx.accounts.validate(&ctx, &params))]
pub fn trigger_function(
ctx: Context<TriggerFunction>,
params: TriggerFunctionParams,
) -> anchor_lang::Result<()> {
TriggerFunction::actuate(&ctx, &params)
}
}

View File

@ -0,0 +1,155 @@
use crate::*;
use bytemuck::{Pod, Zeroable};
#[account(zero_copy(unsafe))]
pub struct MyProgramState {
pub bump: u8,
pub authority: Pubkey,
pub function: Pubkey,
}
#[repr(packed)]
#[zero_copy(unsafe)]
pub struct OracleData {
pub oracle_timestamp: i64,
pub price: i128,
pub volume_1hr: i128,
pub volume_24hr: i128,
pub twap_1hr: i128,
pub twap_24hr: i128,
}
#[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)]
pub struct OracleDataBorsh {
pub oracle_timestamp: i64,
pub price: i128,
pub volume_1hr: i128,
pub volume_24hr: i128,
pub twap_1hr: i128,
pub twap_24hr: i128,
}
impl From<OracleDataBorsh> for OracleData {
fn from(value: OracleDataBorsh) -> Self {
Self {
oracle_timestamp: value.oracle_timestamp,
price: value.price.clone(),
volume_1hr: value.volume_1hr.clone(),
volume_24hr: value.volume_24hr.clone(),
twap_1hr: value.twap_1hr.clone(),
twap_24hr: value.twap_24hr.clone(),
}
}
}
#[derive(Copy, Clone, Default, AnchorSerialize, AnchorDeserialize)]
pub struct OracleDataWithTradingSymbol {
pub symbol: TradingSymbol,
pub data: OracleDataBorsh,
}
impl OracleData {
pub fn get_fair_price(&self) -> anchor_lang::Result<f64> {
// Check the price was updated in the last 10 seconds
// Do some logic here based on the twap
let price: f64 = SwitchboardDecimal {
mantissa: self.price,
scale: 9,
}
.try_into()?;
Ok(price)
}
}
#[repr(packed)]
#[account(zero_copy(unsafe))]
pub struct MyOracleState {
pub bump: u8,
pub btc: OracleData,
pub usdc: OracleData,
pub eth: OracleData,
pub sol: OracleData,
pub doge: OracleData,
// can always re-allocate to add more
// pub reserved: [u8; 2400],
}
impl MyOracleState {
pub fn save_rows(
&mut self,
rows: &Vec<OracleDataWithTradingSymbol>,
) -> anchor_lang::Result<()> {
for row in rows.iter() {
match row.symbol {
TradingSymbol::Btc => {
msg!("saving BTC price, {}", { row.data.price });
self.btc = row.data.into();
}
TradingSymbol::Usdc => {
msg!("saving USDC price, {}", { row.data.price });
self.usdc = row.data.into();
}
TradingSymbol::Eth => {
msg!("saving ETH price, {}", { row.data.price });
self.eth = row.data.into();
}
TradingSymbol::Sol => {
msg!("saving SOL price, {}", { row.data.price });
self.sol = row.data.into();
}
TradingSymbol::Doge => {
msg!("saving DOGE price, {}", { row.data.price });
self.doge = row.data.into();
}
_ => {
msg!("no trading symbol found for {:?}", row.symbol);
// TODO: emit an event so we can detect and fix
}
}
}
Ok(())
}
}
#[repr(u8)]
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, AnchorSerialize, AnchorDeserialize)]
pub enum TradingSymbol {
#[default]
Unknown = 0,
Btc = 1,
Usdc = 2,
Eth = 3,
Sol = 4,
Doge = 5,
}
unsafe impl Pod for TradingSymbol {}
unsafe impl Zeroable for TradingSymbol {}
impl From<TradingSymbol> for u8 {
fn from(value: TradingSymbol) -> Self {
match value {
TradingSymbol::Btc => 1,
TradingSymbol::Usdc => 2,
TradingSymbol::Eth => 3,
TradingSymbol::Sol => 4,
TradingSymbol::Doge => 5,
_ => 0,
}
}
}
impl From<u8> for TradingSymbol {
fn from(value: u8) -> Self {
match value {
1 => TradingSymbol::Btc,
2 => TradingSymbol::Usdc,
3 => TradingSymbol::Eth,
4 => TradingSymbol::Sol,
5 => TradingSymbol::Doge,
_ => TradingSymbol::default(),
}
}
}

View File

@ -0,0 +1,48 @@
pub use crate::*;
pub fn parse_mr_enclaves(enclaves: &Vec<[u8; 32]>) -> anchor_lang::Result<[[u8; 32]; 32]> {
// enclaves
// .clone()
// .try_into()
// .map_err(|_| error!(BasicOracleError::ArrayOverflow))
if enclaves.len() > 32 {
return Err(error!(BasicOracleError::ArrayOverflow));
}
let mut result: [[u8; 32]; 32] = [[0; 32]; 32];
for (i, enclave) in enclaves.iter().enumerate() {
result[i] = *enclave;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mr_enclaves_success() {
let enclaves: Vec<[u8; 32]> = vec![[1; 32]; 10];
let result = parse_mr_enclaves(&enclaves).unwrap();
// Check first 10 elements are [1; 32]
for i in 0..10 {
assert_eq!(result[i], [1; 32]);
}
// Check the remaining elements are [0; 32] (default)
for i in 10..32 {
assert_eq!(result[i], [0; 32]);
}
}
// #[test]
// fn test_parse_mr_enclaves_overflow() {
// let enclaves: Vec<[u8; 32]> = vec![[1; 32]; 33];
// match parse_mr_enclaves(&enclaves) {
// Err(BasicOracleError::ArrayOverflow) => {} // test passes
// _ => panic!("Unexpected result"), // test fails
// };
// }
}

View File

@ -0,0 +1,246 @@
// eslint-disable-next-line node/no-unpublished-import
import type { BasicOracle } from "../target/types/basic_oracle";
import { printLogs } from "./utils";
import type { Program } from "@coral-xyz/anchor";
import * as anchor from "@coral-xyz/anchor";
import { sleep } from "@switchboard-xyz/common";
import type { FunctionAccount, MrEnclave } from "@switchboard-xyz/solana.js";
import { SwitchboardWallet } from "@switchboard-xyz/solana.js";
import {
AttestationProgramStateAccount,
AttestationQueueAccount,
attestationTypes,
type BootstrappedAttestationQueue,
parseMrEnclave,
SwitchboardProgram,
types,
} from "@switchboard-xyz/solana.js";
const unixTimestamp = () => Math.floor(Date.now() / 1000);
// vv1gTnfuUiroqgJHS4xsRASsRQqqixCv1su85VWvcP9
const MRENCLAVE = parseMrEnclave(
Buffer.from("Y6keo0uTCiWDNcWwGjZ2jfTd4VFhrr6LC/6Mk1aiNCA=", "base64")
);
const emptyEnclave: number[] = new Array(32).fill(0);
function has_mr_enclave(
enclaves: Array<MrEnclave>,
unknown_enclave: MrEnclave
) {
return enclaves.includes(unknown_enclave);
}
describe("basic_oracle", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicOracle as Program<BasicOracle>;
const payer = (program.provider as anchor.AnchorProvider).publicKey;
const programStatePubkey = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("BASICORACLE")],
program.programId
)[0];
const oraclePubkey = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("ORACLE_V1_SEED")],
program.programId
)[0];
let switchboard: BootstrappedAttestationQueue;
let wallet: SwitchboardWallet;
let functionAccount: FunctionAccount;
before(async () => {
const switchboardProgram = await SwitchboardProgram.fromProvider(
program.provider as anchor.AnchorProvider
);
await AttestationProgramStateAccount.getOrCreate(switchboardProgram);
switchboard = await AttestationQueueAccount.bootstrapNewQueue(
switchboardProgram
);
console.log(`programStatePubkey: ${programStatePubkey}`);
[wallet] = await SwitchboardWallet.create(
switchboard.program,
switchboard.attestationQueue.publicKey,
payer,
"MySharedWallet",
16
);
console.log(`wallet: ${wallet.publicKey}`);
[functionAccount] =
await switchboard.attestationQueue.account.createFunction(
{
name: "test function",
metadata: "this function handles XYZ for my protocol",
schedule: "15 * * * * *",
container: "switchboardlabs/basic-oracle-function",
version: "latest",
mrEnclave: MRENCLAVE,
authority: programStatePubkey,
},
wallet
);
console.log(`functionAccount: ${functionAccount.publicKey}`);
});
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize({})
.accounts({
program: programStatePubkey,
oracle: oraclePubkey,
authority: payer,
payer: payer,
// function: functionAccount.publicKey,
})
.rpc()
.catch((err) => {
console.error(err);
throw err;
});
console.log("Your transaction signature", tx);
});
// it("Adds an enclave measurement", async () => {
// // Add your test here.
// const tx = await program.methods
// .setEnclaves({ mrEnclaves: [Array.from(MRENCLAVE)] })
// .accounts({
// program: programStatePubkey,
// authority: payer,
// })
// .rpc()
// .catch((err) => {
// console.error(err);
// throw err;
// });
// console.log("Your transaction signature", tx);
// const programState = await program.account.myProgramState.fetch(
// programStatePubkey
// );
// });
it("Oracle refreshes the prices", async () => {
const securedSigner = anchor.web3.Keypair.generate();
const rewardAddress =
await switchboard.program.mint.getOrCreateAssociatedUser(payer);
const functionState = await functionAccount.loadData();
// TODO: generate function verify ixn
const functionVerifyIxn = attestationTypes.functionVerify(
switchboard.program,
{
params: {
observedTime: new anchor.BN(unixTimestamp()),
nextAllowedTimestamp: new anchor.BN(unixTimestamp() + 100),
isFailure: false,
mrEnclave: Array.from(MRENCLAVE),
},
},
{
function: functionAccount.publicKey,
functionEnclaveSigner: securedSigner.publicKey,
verifier: switchboard.verifier.publicKey,
verifierSigner: switchboard.verifier.signer.publicKey,
attestationQueue: switchboard.attestationQueue.publicKey,
escrowWallet: functionState.escrowWallet,
escrowTokenWallet: functionState.escrowTokenWallet,
receiver: rewardAddress,
verifierPermission: switchboard.verifier.permissionAccount.publicKey,
tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
}
);
// Add your test here.
const tx = await program.methods
.refreshOracles({
rows: [
{
symbol: { btc: {} },
data: {
oracleTimestamp: new anchor.BN(unixTimestamp()),
price: new anchor.BN("25225000000000"), // 25225
volume1hr: new anchor.BN("25225000000000"), // 1337000
volume24hr: new anchor.BN("25225000000000"), // 1337000
twap1hr: new anchor.BN("25225000000000"), // 25550
twap24hr: new anchor.BN("25225000000000"), // 25550
},
},
{
symbol: { eth: {} },
data: {
oracleTimestamp: new anchor.BN(unixTimestamp()),
price: new anchor.BN("1750000000000"), // 1750
volume1hr: new anchor.BN("420000000000"), // 420000
volume24hr: new anchor.BN("420000000000"), // 420000
twap1hr: new anchor.BN("1750000000000"), // 1750
twap24hr: new anchor.BN("1750000000000"), // 1750
},
},
],
})
.accounts({
oracle: oraclePubkey,
function: functionAccount.publicKey,
enclaveSigner: securedSigner.publicKey,
})
.preInstructions([functionVerifyIxn])
.signers([switchboard.verifier.signer, securedSigner])
.rpc({ skipPreflight: true });
console.log("Your transaction signature", tx);
await printLogs(switchboard.program.connection, tx ? tx : "");
await sleep(5000);
const oracleState = await program.account.myOracleState.fetch(oraclePubkey);
console.log(oracleState);
console.log(`BTC\n`);
printData(oracleState.btc);
console.log(`ETH\n`);
printData(oracleState.eth);
console.log(`SOL\n`);
printData(oracleState.sol);
});
});
function normalizeDecimals(value: anchor.BN) {
return (value ?? new anchor.BN(0))
.div(new anchor.BN(10).pow(new anchor.BN(9)))
.toNumber();
}
function printData(obj: {
oracleTimestamp: anchor.BN;
price: anchor.BN;
volume1hr: anchor.BN;
volume24hr: anchor.BN;
twap1hr: anchor.BN;
twap24hr: anchor.BN;
}) {
console.log(`\tprice: ${normalizeDecimals(obj.price)}`);
console.log(`\ttimestamp: ${obj.oracleTimestamp.toNumber()}`);
console.log(`\t1Hr Volume: ${normalizeDecimals(obj.volume1hr)}`);
console.log(`\t24Hr Volume: ${normalizeDecimals(obj.volume24hr)}`);
console.log(`\t1Hr Twap: ${normalizeDecimals(obj.twap1hr)}`);
console.log(`\t24Hr Twap: ${normalizeDecimals(obj.twap24hr)}`);
}

View File

@ -0,0 +1,16 @@
import type { Connection } from "@solana/web3.js";
import { sleep } from "@switchboard-xyz/common";
export async function printLogs(
connection: Connection,
tx: string,
v0Txn?: boolean,
delay = 3000
) {
await sleep(delay);
const parsed = await connection.getParsedTransaction(tx, {
commitment: "confirmed",
maxSupportedTransactionVersion: v0Txn ? 0 : undefined,
});
console.log(parsed?.meta?.logMessages?.join("\n"));
}

View File

@ -0,0 +1,27 @@
{
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
},
"compilerOptions": {
"types": ["mocha", "chai", "node"],
"typeRoots": ["./node_modules/@types"],
"module": "commonjs",
"noEmit": true,
"esModuleInterop": true,
"strict": false,
"strictNullChecks": false,
"target": "es6",
"paths": {
"@switchboard-xyz/solana.js": ["../../../javascript/solana.js"]
}
},
"include": ["tests/**/*", "target/types/*.ts"],
"exclude": ["target", "lib"],
"references": [
{
"path": "../../../../../javascript/solana.js"
}
]
}

View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
**/*.rs.bk
node_modules
test-ledger
.yarn

View File

@ -0,0 +1,8 @@
.anchor
.DS_Store
target
node_modules
dist
build
test-ledger

View File

@ -0,0 +1,47 @@
[features]
seeds = false
skip-lint = false
[programs.localnet]
custom_randomness_request = "Csx5AU83fPiaSChJUBZg2cW9GcCVVwZ4rwFqDA2pomX2"
[programs.devnet]
custom_randomness_request = "Csx5AU83fPiaSChJUBZg2cW9GcCVVwZ4rwFqDA2pomX2"
[registry]
url = "https://api.apr.dev"
[provider]
# cluster = "Localnet"
cluster = "https://api.devnet.solana.com"
wallet = "~/.config/solana/id.json"
[scripts]
test = "pnpm exec ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
[test.validator]
url = "https://api.devnet.solana.com"
[test]
startup_wait = 15000
[[test.validator.clone]] # sb devnet oracle programID
address = "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
[[test.validator.clone]] # sb devnet oracle IDL
address = "Fi8vncGpNKbq62gPo56G4toCehWNy77GgqGkTaAF5Lkk"
[[test.validator.clone]] # sb devnet oracle SbState
address = "CyZuD7RPDcrqCGbNvLCyqk6Py9cEZTKmNKujfPi3ynDd"
[[test.validator.clone]] # sb devnet oracle tokenVault
address = "7hkp1xfPBcD2t1vZMoWWQPzipHVcXeLAAaiGXdPSfDie"
[[test.validator.clone]] # sb devnet attestation programID
address = "sbattyXrzedoNATfc4L31wC9Mhxsi1BmFhTiN8gDshx"
[[test.validator.clone]] # sb devnet attestation IDL
address = "5ExuoQR69trmKQfB95fDsUGsUrrChbGq9PFgt8qouncz"
[[test.validator.clone]] # sb devnet programState
address = "5MFs7RGTjLi1wtKNBFRtuLipCkkjs4YQwRRU9sjnbQbS"

Some files were not shown because too many files have changed in this diff Show More