Pyth oracle integration

Learn how to test smart contracts that depend on Pyth oracle price feeds using Clarinet.


What you'll learn

In this guide, you'll learn how to test Clarity contracts that integrate with Pyth oracle price feeds.

Setting up mainnet contract dependencies
Using mainnet transaction execution (MXS) for oracle testing
Creating mock price feeds for unit tests
Writing integration tests with real oracle data

Prerequisites

To follow this guide, you'll need:

Clarinet version 2.1.0 or higher with MXS support
Understanding of Pyth oracle contracts

Quickstart

Configure mainnet dependencies

Add the Pyth oracle contracts to your Clarinet.toml requirements:

Clarinet.toml
[project]
name = "my-oracle-project"
[[project.requirements]]
contract_id = "SP3R4F6C1J3JQWWCVZ3S7FRRYPMYG6ZW6RZK31FXY.pyth-oracle-v3"
[[project.requirements]]
contract_id = "SP3R4F6C1J3JQWWCVZ3S7FRRYPMYG6ZW6RZK31FXY.pyth-storage-v3"
[[project.requirements]]
contract_id = "SP3R4F6C1J3JQWWCVZ3S7FRRYPMYG6ZW6RZK31FXY.pyth-pnau-decoder-v2"
[[project.requirements]]
contract_id = "SP3R4F6C1J3JQWWCVZ3S7FRRYPMYG6ZW6RZK31FXY.wormhole-core-v3"

This allows your tests to interact with the actual mainnet oracle contracts.

Create a mock oracle for unit tests

For fast unit tests, create a mock oracle contract:

tests/mocks/mock-pyth-oracle.clar
;; Mock Pyth Oracle for testing
(define-map price-feeds
(buff 32) ;; price-feed-id
{
price: int,
conf: uint,
expo: int,
ema-price: int,
ema-conf: uint,
publish-time: uint,
prev-publish-time: uint
}
)
;; Set a mock price
(define-public (set-mock-price
(feed-id (buff 32))
(price int)
(expo int))
(ok (map-set price-feeds feed-id {
price: price,
conf: u1000000,
expo: expo,
ema-price: price,
ema-conf: u1000000,
publish-time: block-height,
prev-publish-time: (- block-height u1)
}))
)
;; Mock get-price function matching real oracle interface
(define-read-only (get-price
(feed-id (buff 32))
(storage-contract principal))
(ok (unwrap!
(map-get? price-feeds feed-id)
(err u404)))
)
;; Mock verify-and-update-price-feeds (no-op for testing)
(define-public (verify-and-update-price-feeds
(vaa (buff 8192))
(params (tuple)))
(ok u1)
)

Write unit tests with mocked prices

Create unit tests using the mock oracle:

tests/benjamin-club.test.ts
import { Cl } from '@stacks/transactions';
import { describe, expect, it } from "vitest";
const accounts = simnet.getAccounts();
const deployer = accounts.get("deployer")!;
const wallet1 = accounts.get("wallet_1")!;
describe("Benjamin Club with Mock Oracle", () => {
it("should set up mock BTC price", () => {
// Set BTC price to $100,000 (with 8 decimals)
const btcFeedId = "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";
const mockPrice = Cl.int(10000000000000); // $100,000.00 * 10^8
const expo = Cl.int(-8);
const setPriceResponse = simnet.callPublicFn(
"mock-pyth-oracle",
"set-mock-price",
[Cl.bufferFromHex(btcFeedId), mockPrice, expo],
deployer
);
expect(setPriceResponse.result).toBeOk(Cl.uint(1));
});
it("should calculate correct sBTC amount for $100", () => {
// With BTC at $100,000, $100 = 0.001 BTC
const response = simnet.callReadOnlyFn(
"benjamin-club",
"get-required-sbtc-amount",
[],
wallet1
);
// 0.001 BTC = 100,000 sats
expect(response.result).toBeOk(Cl.uint(100000));
});
it("should mint NFT when user has enough sBTC", () => {
// Mock VAA data (can be dummy data for unit tests)
const mockVaa = Cl.bufferFromHex("00".repeat(100));
// Give user 1 BTC worth of sBTC
simnet.callPublicFn(
"sbtc-token",
"mint",
[Cl.uint(100000000), Cl.principal(wallet1)],
deployer
);
const mintResponse = simnet.callPublicFn(
"benjamin-club",
"mint-for-hundred-dollars",
[mockVaa],
wallet1
);
expect(mintResponse.result).toBeOk(Cl.uint(1));
});
});

Set up mainnet execution tests

For integration tests with real oracle data, use mainnet transaction execution:

tests/benjamin-club-mxs.test.ts
import { buildDevnetNetworkOrchestrator } from "@hirosystems/clarinet-sdk";
import { Cl } from '@stacks/transactions';
import { PriceServiceConnection } from '@pythnetwork/price-service-client';
import { describe, expect, it } from "vitest";
describe("Benjamin Club with Mainnet Oracle", () => {
let orchestrator: any;
beforeAll(async () => {
orchestrator = buildDevnetNetworkOrchestrator({
// Point to a mainnet fork
manifest: "./Clarinet.toml",
networkId: 1 // Mainnet
});
});
it("should interact with real Pyth oracle", async () => {
// Fetch real VAA from Pyth
const pythClient = new PriceServiceConnection(
"https://hermes.pyth.network",
{ priceFeedRequestConfig: { binary: true } }
);
const btcFeedId = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";
const vaas = await pythClient.getLatestVaas([btcFeedId]);
const vaaHex = Buffer.from(vaas[0], 'base64').toString('hex');
// Use mainnet execution to test with real oracle
const response = await orchestrator.callPublicFn(
"benjamin-club",
"get-current-btc-price",
[Cl.bufferFromHex(vaaHex)],
"wallet_1"
);
expect(response.result).toBeOk();
// Verify price is reasonable (between $10k and $200k)
const priceData = response.result.value;
const price = priceData.data.price.value;
expect(price).toBeGreaterThan(1000000000000); // $10k
expect(price).toBeLessThan(20000000000000); // $200k
});
});

Common patterns

Testing different price scenarios

Create helper functions to test various market conditions:

tests/helpers/oracle-helpers.ts
export function setupPriceScenario(
scenario: 'bull' | 'bear' | 'stable',
simnet: any
) {
const prices = {
bull: {
btc: 15000000000000, // $150,000
stx: 500000000, // $5.00
eth: 500000000000 // $5,000
},
bear: {
btc: 2000000000000, // $20,000
stx: 50000000, // $0.50
eth: 100000000000 // $1,000
},
stable: {
btc: 10000000000000, // $100,000
stx: 200000000, // $2.00
eth: 300000000000 // $3,000
}
};
const selectedPrices = prices[scenario];
// Set up all price feeds
Object.entries(selectedPrices).forEach(([asset, price]) => {
const feedId = getFeedId(asset);
simnet.callPublicFn(
"mock-pyth-oracle",
"set-mock-price",
[Cl.bufferFromHex(feedId), Cl.int(price), Cl.int(-8)],
"deployer"
);
});
}

Testing price staleness

Ensure your contract handles stale prices correctly:

tests/price-staleness.test.ts
describe("Price Staleness Handling", () => {
it("should reject stale prices", () => {
// Set a price with old timestamp
const oldTimestamp = simnet.getBlockHeight() - 1000;
simnet.callPublicFn(
"mock-pyth-oracle",
"set-price-with-timestamp",
[
Cl.bufferFromHex(BTC_FEED_ID),
Cl.int(10000000000000),
Cl.int(-8),
Cl.uint(oldTimestamp)
],
deployer
);
const response = simnet.callPublicFn(
"benjamin-club",
"mint-for-hundred-dollars",
[mockVaa],
wallet1
);
expect(response.result).toBeErr(Cl.uint(ERR_STALE_PRICE));
});
});

Deployment testing

Create deployment tests to ensure proper configuration:

deployments/mainnet.yaml
---
id: 0
name: "Mainnet deployment"
network: mainnet
stacks-node: "https://api.mainnet.hiro.so"
bitcoin-node: "https://blockstream.info/api/"
plan:
batches:
- id: 0
transactions:
- contract-call:
contract-id: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRCBGD7R.benjamin-club
method: initialize
parameters:
- "'SP3R4F6C1J3JQWWCVZ3S7FRRYPMYG6ZW6RZK31FXY.pyth-oracle-v3"
- "'SP3R4F6C1J3JQWWCVZ3S7FRRYPMYG6ZW6RZK31FXY.pyth-storage-v3"

Gas estimation

Test gas consumption for oracle operations:

tests/gas-estimation.test.ts
describe("Gas Estimation", () => {
it("should estimate gas for oracle update + mint", () => {
const txCost = simnet.getTransactionCost(
"benjamin-club",
"mint-for-hundred-dollars",
[Cl.bufferFromHex("00".repeat(1000))], // Typical VAA size
wallet1
);
console.log("Estimated gas cost:", txCost);
// Ensure gas cost is reasonable
expect(txCost.write_length).toBeLessThan(20000);
expect(txCost.runtime).toBeLessThan(50000000);
});
});

CI/CD integration

Set up GitHub Actions for automated testing:

.github/workflows/test-oracle.yml
name: Test Oracle Integration
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Clarinet
run: |
curl -L https://github.com/hirosystems/clarinet/releases/download/v2.1.0/clarinet-linux-x64.tar.gz | tar xz
chmod +x ./clarinet
sudo mv ./clarinet /usr/local/bin
- name: Run unit tests
run: clarinet test --coverage
- name: Run mainnet fork tests
run: clarinet test --mainnet-fork
env:
MAINNET_API_KEY: ${{ secrets.MAINNET_API_KEY }}