1
+ import { Connection , PublicKey , clusterApiUrl , Cluster , Commitment , AccountInfo , Account } from '@solana/web3.js'
2
+ import {
3
+ Base , Magic ,
4
+ parseMappingData ,
5
+ parseBaseData ,
6
+ parsePriceData ,
7
+ parseProductData , Price , PriceData , Product , ProductData ,
8
+ Version , AccountType ,
9
+ } from './index'
10
+
11
+ const ONES = '11111111111111111111111111111111'
12
+
13
+ /**
14
+ * Type of callback invoked whenever a pyth price account changes. The callback additionally
15
+ * gets access product, which contains the metadata for this price account (e.g., that the symbol is "BTC/USD")
16
+ */
17
+ export type PythPriceCallback = ( product : Product , price : PriceData ) => void
18
+
19
+ /**
20
+ * Reads Pyth price data from a solana web3 connection. This class uses a callback-driven model,
21
+ * similar to the solana web3 methods for tracking updates to accounts.
22
+ */
23
+ export class PythConnection {
24
+ connection : Connection
25
+ pythProgramKey : PublicKey
26
+ commitment : Commitment
27
+
28
+ productAccountKeyToProduct : Record < string , Product > = { }
29
+ priceAccountKeyToProductAccountKey : Record < string , string > = { }
30
+
31
+ callbacks : PythPriceCallback [ ] = [ ]
32
+
33
+ private handleProductAccount ( key : PublicKey , account : AccountInfo < Buffer > ) {
34
+ const { priceAccountKey, type, product} = parseProductData ( account . data )
35
+ this . productAccountKeyToProduct [ key . toString ( ) ] = product
36
+ if ( priceAccountKey . toString ( ) !== ONES ) {
37
+ this . priceAccountKeyToProductAccountKey [ priceAccountKey . toString ( ) ] = key . toString ( )
38
+ }
39
+ }
40
+
41
+ private handlePriceAccount ( key : PublicKey , account : AccountInfo < Buffer > ) {
42
+ const product = this . productAccountKeyToProduct [ this . priceAccountKeyToProductAccountKey [ key . toString ( ) ] ]
43
+ if ( product === undefined ) {
44
+ // This shouldn't happen since we're subscribed to all of the program's accounts,
45
+ // but let's be good defensive programmers.
46
+ throw new Error ( 'Got a price update for an unknown product. This is a bug in the library, please report it to the developers.' )
47
+ }
48
+
49
+ const priceData = parsePriceData ( account . data )
50
+ for ( let callback of this . callbacks ) {
51
+ callback ( product , priceData )
52
+ }
53
+ }
54
+
55
+ private handleAccount ( key : PublicKey , account : AccountInfo < Buffer > , productOnly : boolean ) {
56
+ const base = parseBaseData ( account . data )
57
+ // The pyth program owns accounts that don't contain pyth data, which we can safely ignore.
58
+ if ( base ) {
59
+ switch ( AccountType [ base . type ] ) {
60
+ case 'Mapping' :
61
+ // We can skip these because we're going to get every account owned by this program anyway.
62
+ break ;
63
+ case 'Product' :
64
+ this . handleProductAccount ( key , account )
65
+ break ;
66
+ case 'Price' :
67
+ if ( ! productOnly ) {
68
+ this . handlePriceAccount ( key , account )
69
+ }
70
+ break ;
71
+ case 'Test' :
72
+ break ;
73
+ default :
74
+ throw new Error ( `Unknown account type: ${ base . type } . Try upgrading pyth-client.` )
75
+ }
76
+ }
77
+ }
78
+
79
+ /** Create a PythConnection that reads its data from an underlying solana web3 connection.
80
+ * pythProgramKey is the public key of the Pyth program running on the chosen solana cluster.
81
+ */
82
+ constructor ( connection : Connection , pythProgramKey : PublicKey , commitment : Commitment = 'finalized' ) {
83
+ this . connection = connection
84
+ this . pythProgramKey = pythProgramKey
85
+ this . commitment = commitment
86
+ }
87
+
88
+ /** Start receiving price updates. Once this method is called, any registered callbacks will be invoked
89
+ * each time a Pyth price account is updated.
90
+ */
91
+ public async start ( ) {
92
+ const accounts = await this . connection . getProgramAccounts ( this . pythProgramKey , this . commitment )
93
+ for ( let account of accounts ) {
94
+ this . handleAccount ( account . pubkey , account . account , true )
95
+ }
96
+
97
+ this . connection . onProgramAccountChange (
98
+ this . pythProgramKey ,
99
+ ( keyedAccountInfo , context ) => {
100
+ this . handleAccount ( keyedAccountInfo . accountId , keyedAccountInfo . accountInfo , false )
101
+ } ,
102
+ this . commitment ,
103
+ )
104
+ }
105
+
106
+ /** Register callback to receive price updates. */
107
+ public onPriceChange ( callback : PythPriceCallback ) {
108
+ this . callbacks . push ( callback )
109
+ }
110
+
111
+ /** Stop receiving price updates. Note that this also currently deletes all registered callbacks. */
112
+ public async stop ( ) {
113
+ // There's no way to actually turn off the solana web3 subscription x_x, but there should be.
114
+ // Leave this method in so we don't have to update our API when solana fixes theirs.
115
+ // In the interim, delete callbacks.
116
+ this . callbacks = [ ]
117
+ }
118
+ }
0 commit comments