Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 2.8.0
- Added `onPriceChangeVerbose` callback to `PythConnection` to support getting account keys and slots on each price update.

## 2.7.2
### Changed
- Added pythtest program key and cluster url
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/client",
"version": "2.7.3",
"version": "2.8.0",
"description": "Client for consuming Pyth price data",
"homepage": "https://pyth.network",
"main": "lib/index.js",
Expand Down
66 changes: 46 additions & 20 deletions src/PythConnection.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
import { Connection, PublicKey, clusterApiUrl, Cluster, Commitment, AccountInfo, Account } from '@solana/web3.js'
import { Connection, PublicKey, Commitment, AccountInfo } from '@solana/web3.js'
import {
Base,
Magic,
parseMappingData,
parseBaseData,
parsePriceData,
parseProductData,
Price,
PriceData,
Product,
ProductData,
Version,
AccountType,
MAX_SLOT_DIFFERENCE,
PriceStatus,
} from './index'

const ONES = '11111111111111111111111111111111'

/** An update to the content of the solana account at `key` that occurred at `slot`. */
export type AccountUpdate<T> = {
key: PublicKey
accountInfo: AccountInfo<T>
slot: number
}

/**
* Type of callback invoked whenever a pyth price account changes. The callback additionally
* gets access product, which contains the metadata for this price account (e.g., that the symbol is "BTC/USD")
* gets `product`, which contains the metadata for this price account (e.g., that the symbol is "BTC/USD")
*/
export type PythPriceCallback = (product: Product, price: PriceData) => void

/**
* A price callback that additionally includes the raw solana account information. Use this if you need
* access to account keys and such.
*/
export type PythVerbosePriceCallback = (product: AccountUpdate<ProductData>, price: AccountUpdate<PriceData>) => void

/**
* Reads Pyth price data from a solana web3 connection. This class uses a callback-driven model,
* similar to the solana web3 methods for tracking updates to accounts.
Expand All @@ -33,22 +39,29 @@ export class PythConnection {
pythProgramKey: PublicKey
commitment: Commitment

productAccountKeyToProduct: Record<string, Product> = {}
productAccountKeyToProduct: Record<string, AccountUpdate<ProductData>> = {}
priceAccountKeyToProductAccountKey: Record<string, string> = {}

callbacks: PythPriceCallback[] = []
callbacks: PythVerbosePriceCallback[] = []

private handleProductAccount(key: PublicKey, account: AccountInfo<Buffer>) {
const { priceAccountKey, type, product } = parseProductData(account.data)
this.productAccountKeyToProduct[key.toString()] = product
if (priceAccountKey.toString() !== ONES) {
this.priceAccountKeyToProductAccountKey[priceAccountKey.toString()] = key.toString()
private handleProductAccount(key: PublicKey, account: AccountInfo<Buffer>, slot: number) {
const productData = parseProductData(account.data)
this.productAccountKeyToProduct[key.toString()] = {
key,
slot,
accountInfo: {
...account,
data: productData,
},
}
if (productData.priceAccountKey.toString() !== ONES) {
this.priceAccountKeyToProductAccountKey[productData.priceAccountKey.toString()] = key.toString()
}
}

private handlePriceAccount(key: PublicKey, account: AccountInfo<Buffer>, slot: number) {
const product = this.productAccountKeyToProduct[this.priceAccountKeyToProductAccountKey[key.toString()]]
if (product === undefined) {
const productUpdate = this.productAccountKeyToProduct[this.priceAccountKeyToProductAccountKey[key.toString()]]
if (productUpdate === undefined) {
// This shouldn't happen since we're subscribed to all of the program's accounts,
// but let's be good defensive programmers.
throw new Error(
Expand All @@ -57,9 +70,17 @@ export class PythConnection {
}

const priceData = parsePriceData(account.data, slot)
const priceUpdate = {
key,
slot,
accountInfo: {
...account,
data: priceData,
},
}

for (const callback of this.callbacks) {
callback(product, priceData)
callback(productUpdate, priceUpdate)
}
}

Expand All @@ -72,7 +93,7 @@ export class PythConnection {
// We can skip these because we're going to get every account owned by this program anyway.
break
case AccountType.Product:
this.handleProductAccount(key, account)
this.handleProductAccount(key, account, slot)
break
case AccountType.Price:
if (!productOnly) {
Expand Down Expand Up @@ -117,6 +138,11 @@ export class PythConnection {

/** Register callback to receive price updates. */
public onPriceChange(callback: PythPriceCallback) {
this.callbacks.push((product, price) => callback(product.accountInfo.data.product, price.accountInfo.data))
}

/** Register a verbose callback to receive price updates. */
public onPriceChangeVerbose(callback: PythVerbosePriceCallback) {
this.callbacks.push(callback)
}

Expand Down
7 changes: 5 additions & 2 deletions src/example_ws_usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { PythConnection } from './PythConnection'
import { getPythClusterApiUrl, getPythProgramKeyForCluster, PythCluster } from './cluster'
import { PriceStatus } from '.'

const SOLANA_CLUSTER_NAME: PythCluster = 'mainnet-beta'
const SOLANA_CLUSTER_NAME: PythCluster = 'devnet'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by: this example fails when pointed to mainnet because the default solana mainnet RPC endpoint no longer accepts program subscribe calls.

const connection = new Connection(getPythClusterApiUrl(SOLANA_CLUSTER_NAME))
const pythPublicKey = getPythProgramKeyForCluster(SOLANA_CLUSTER_NAME)

const pythConnection = new PythConnection(connection, pythPublicKey)
pythConnection.onPriceChange((product, price) => {
pythConnection.onPriceChangeVerbose((productAccount, priceAccount) => {
// The arguments to the callback include solana account information / the update slot if you need it.
const product = productAccount.accountInfo.data.product;
const price = priceAccount.accountInfo.data;
Comment on lines +11 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we also wanna keep the onPriceChange example or do we want to encourage new users to use onPriceChangeVerbose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most people will prefer the verbose function (because you almost always need the account keys), hence why i used that in the example.

// sample output:
// SRM/USD: $8.68725 ±$0.0131
if (price.price && price.confidence) {
Expand Down