usePyth.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import {
  2. parseMappingData,
  3. parsePriceData,
  4. parseProductData,
  5. PriceData,
  6. ProductData,
  7. } from '@pythnetwork/client'
  8. import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'
  9. import { Buffer } from 'buffer'
  10. import { SetStateAction, useContext, useEffect, useRef, useState } from 'react'
  11. import { ClusterContext } from '../contexts/ClusterContext'
  12. import { pythClusterApiUrls } from '../utils/pythClusterApiUrl'
  13. const ONES = '11111111111111111111111111111111'
  14. function chunks<T>(array: T[], size: number): T[][] {
  15. return Array.apply(0, new Array(Math.ceil(array.length / size))).map(
  16. (_, index) => array.slice(index * size, (index + 1) * size)
  17. )
  18. }
  19. const getMultipleAccountsCore = async (
  20. connection: Connection,
  21. keys: string[],
  22. commitment: string
  23. ) => {
  24. //keys are initially base58 encoded
  25. const pubkeyTransform = keys.map((x) => new PublicKey(x))
  26. const resultArray = await connection.getMultipleAccountsInfo(
  27. pubkeyTransform,
  28. commitment as Commitment
  29. )
  30. return { keys, array: resultArray }
  31. }
  32. const getMultipleAccounts = async (
  33. connection: Connection,
  34. keys: string[],
  35. commitment: string
  36. ) => {
  37. const result = await Promise.all(
  38. chunks(keys, 99).map((chunk) =>
  39. getMultipleAccountsCore(connection, chunk, commitment)
  40. )
  41. )
  42. const array = result
  43. .map(
  44. (a) =>
  45. a.array
  46. .map((acc) => {
  47. if (!acc) {
  48. return undefined
  49. } else {
  50. return acc
  51. }
  52. })
  53. .filter((_) => _) as AccountInfo<Buffer>[]
  54. )
  55. .flat()
  56. return { keys, array }
  57. }
  58. export const ORACLE_PUBLIC_KEYS: { [key: string]: string } = {
  59. devnet: 'BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2',
  60. testnet: 'AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z',
  61. 'mainnet-beta': 'AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J',
  62. pythtest: 'AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z',
  63. pythnet: 'AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J',
  64. }
  65. export const BAD_SYMBOLS = [undefined]
  66. const createSetSymbolMapUpdater =
  67. (
  68. symbol: string | number,
  69. product: ProductData,
  70. price: PriceData,
  71. productAccountKey: any,
  72. priceAccountKey: any
  73. ) =>
  74. (prev: { [x: string]: { price: { [x: string]: number } } }) =>
  75. !prev[symbol] || prev[symbol].price['validSlot'] < price.validSlot
  76. ? {
  77. ...prev,
  78. [symbol]: {
  79. product,
  80. price,
  81. productAccountKey,
  82. priceAccountKey,
  83. },
  84. }
  85. : prev
  86. const handlePriceInfo = (
  87. symbol: string,
  88. product: ProductData,
  89. accountInfo: {
  90. executable?: boolean
  91. owner?: PublicKey
  92. lamports?: number
  93. data: Buffer
  94. rentEpoch?: number | undefined
  95. },
  96. setSymbolMap: {
  97. (value: SetStateAction<{}>): void
  98. (value: SetStateAction<{}>): void
  99. (
  100. arg0: (prev: { [x: string]: { price: { [x: string]: number } } }) => {
  101. [x: string]:
  102. | { price: { [x: string]: number } }
  103. | {
  104. product: ProductData
  105. price: PriceData
  106. productAccountKey: number
  107. priceAccountKey: number
  108. }
  109. }
  110. ): void
  111. },
  112. productAccountKey: string,
  113. priceAccountKey: PublicKey,
  114. setPriceAccounts: {
  115. (value: SetStateAction<{}>): void
  116. (value: SetStateAction<{}>): void
  117. (arg0: (o: any) => any): void
  118. }
  119. ) => {
  120. if (!accountInfo || !accountInfo.data) return
  121. const price = parsePriceData(accountInfo.data)
  122. setPriceAccounts((o) => ({
  123. ...o,
  124. [priceAccountKey.toString()]: {
  125. isLoading: false,
  126. error: null,
  127. price,
  128. },
  129. }))
  130. if (price.priceType !== 1)
  131. console.log(symbol, price.priceType, price.nextPriceAccountKey!.toString)
  132. setSymbolMap(
  133. createSetSymbolMapUpdater(
  134. symbol,
  135. product,
  136. price,
  137. productAccountKey,
  138. priceAccountKey
  139. )
  140. )
  141. }
  142. interface IProductAccount {
  143. isLoading: boolean
  144. error: any // TODO: fix any
  145. product: any // TODO: fix any
  146. }
  147. interface PythHookData {
  148. isLoading: boolean
  149. error: any // TODO: fix any
  150. version: number | null
  151. numProducts: number
  152. productAccounts: { [key: string]: IProductAccount }
  153. priceAccounts: any // TODO: fix any
  154. symbolMap: any // TODO: fix any
  155. connection?: Connection
  156. }
  157. const usePyth = (
  158. symbolFilter?: Array<String>,
  159. subscribe = true
  160. ): PythHookData => {
  161. const connectionRef = useRef<Connection>()
  162. const { cluster } = useContext(ClusterContext)
  163. const oraclePublicKey = ORACLE_PUBLIC_KEYS[cluster]
  164. const [isLoading, setIsLoading] = useState(true)
  165. const [error, setError] = useState(null)
  166. const [version, setVersion] = useState<number | null>(null)
  167. const [urlsIndex, setUrlsIndex] = useState(0)
  168. const [numProducts, setNumProducts] = useState(0)
  169. const [productAccounts, setProductAccounts] = useState({})
  170. const [priceAccounts, setPriceAccounts] = useState({})
  171. const [symbolMap, setSymbolMap] = useState({})
  172. useEffect(() => {
  173. setIsLoading(true)
  174. setError(null)
  175. setVersion(null)
  176. setNumProducts(0)
  177. setProductAccounts({})
  178. setPriceAccounts({})
  179. setSymbolMap({})
  180. }, [urlsIndex, oraclePublicKey, cluster])
  181. useEffect(() => {
  182. let cancelled = false
  183. const subscriptionIds: number[] = []
  184. const urls = pythClusterApiUrls(cluster)
  185. const connection = new Connection(urls[urlsIndex].rpcUrl, {
  186. commitment: 'confirmed',
  187. wsEndpoint: urls[urlsIndex].wsUrl,
  188. })
  189. connectionRef.current = connection
  190. ;(async () => {
  191. // read mapping account
  192. const publicKey = new PublicKey(oraclePublicKey)
  193. try {
  194. const accountInfo = await connection.getAccountInfo(publicKey)
  195. if (cancelled) return
  196. if (!accountInfo || !accountInfo.data) {
  197. setIsLoading(false)
  198. return
  199. }
  200. const { productAccountKeys, version, nextMappingAccount } =
  201. parseMappingData(accountInfo.data)
  202. let allProductAccountKeys = [...productAccountKeys]
  203. let anotherMappingAccount = nextMappingAccount
  204. while (anotherMappingAccount) {
  205. const accountInfo = await connection.getAccountInfo(
  206. anotherMappingAccount
  207. )
  208. if (cancelled) return
  209. if (!accountInfo || !accountInfo.data) {
  210. anotherMappingAccount = null
  211. } else {
  212. const { productAccountKeys, nextMappingAccount } = parseMappingData(
  213. accountInfo.data
  214. )
  215. allProductAccountKeys = [
  216. ...allProductAccountKeys,
  217. ...productAccountKeys,
  218. ]
  219. anotherMappingAccount = nextMappingAccount
  220. }
  221. }
  222. setIsLoading(false)
  223. setVersion(version)
  224. setNumProducts(allProductAccountKeys.length)
  225. setProductAccounts(
  226. allProductAccountKeys.reduce((o, p) => {
  227. // @ts-ignore
  228. o[p.toString()] = { isLoading: true, error: null, product: null }
  229. return o
  230. }, {})
  231. )
  232. const productsInfos = await getMultipleAccounts(
  233. connection,
  234. allProductAccountKeys.map((p) => p.toBase58()),
  235. 'confirmed'
  236. )
  237. if (cancelled) return
  238. const productsData = productsInfos.array.map((p) =>
  239. parseProductData(p.data)
  240. )
  241. const priceInfos = await getMultipleAccounts(
  242. connection,
  243. productsData
  244. .filter((x) => x.priceAccountKey.toString() !== ONES)
  245. .map((p) => p.priceAccountKey.toBase58()),
  246. 'confirmed'
  247. )
  248. if (cancelled) return
  249. for (let i = 0; i < productsInfos.keys.length; i++) {
  250. const productAccountKey = productsInfos.keys[i]
  251. const product = productsData[i]
  252. const symbol = product.product.symbol
  253. const priceAccountKey = product.priceAccountKey
  254. const priceInfo = priceInfos.array[i]
  255. setProductAccounts((o) => ({
  256. ...o,
  257. [productAccountKey.toString()]: {
  258. isLoading: false,
  259. error: null,
  260. product,
  261. },
  262. }))
  263. if (
  264. priceAccountKey.toString() !== ONES &&
  265. (!symbolFilter || symbolFilter.includes(symbol)) &&
  266. // @ts-ignore
  267. !BAD_SYMBOLS.includes(symbol)
  268. ) {
  269. // TODO: we can add product info here and update the price later
  270. setPriceAccounts((o) => ({
  271. ...o,
  272. [priceAccountKey.toString()]: {
  273. isLoading: true,
  274. error: null,
  275. price: null,
  276. },
  277. }))
  278. handlePriceInfo(
  279. symbol,
  280. product,
  281. priceInfo,
  282. setSymbolMap,
  283. productAccountKey,
  284. priceAccountKey,
  285. setPriceAccounts
  286. )
  287. if (subscribe) {
  288. subscriptionIds.push(
  289. connection.onAccountChange(priceAccountKey, (accountInfo) => {
  290. if (cancelled) return
  291. handlePriceInfo(
  292. symbol,
  293. product,
  294. accountInfo,
  295. setSymbolMap,
  296. productAccountKey,
  297. priceAccountKey,
  298. setPriceAccounts
  299. )
  300. })
  301. )
  302. }
  303. }
  304. }
  305. setIsLoading(false)
  306. } catch (e) {
  307. if (cancelled) return
  308. if (urlsIndex === urls.length - 1) {
  309. // @ts-ignore
  310. setError(e)
  311. setIsLoading(false)
  312. console.warn(
  313. `Failed to fetch mapping info for ${publicKey.toString()}`
  314. )
  315. } else if (urlsIndex < urls.length - 1) {
  316. setUrlsIndex((urlsIndex) => urlsIndex + 1)
  317. }
  318. }
  319. })()
  320. return () => {
  321. cancelled = true
  322. for (const subscriptionId of subscriptionIds) {
  323. connection.removeAccountChangeListener(subscriptionId).catch(() => {
  324. console.warn(
  325. `Unsuccessfully attempted to remove listener for subscription id ${subscriptionId}`
  326. )
  327. })
  328. }
  329. }
  330. }, [symbolFilter, urlsIndex, oraclePublicKey, cluster, subscribe])
  331. return {
  332. isLoading,
  333. error,
  334. version,
  335. numProducts,
  336. productAccounts,
  337. priceAccounts,
  338. symbolMap,
  339. connection: connectionRef.current,
  340. }
  341. }
  342. export default usePyth