oracleUtils.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { Buffer } from 'buffer'
  2. import { BN, Program, web3 } from '@project-serum/anchor'
  3. export const Magic = 0xa1b2c3d4
  4. export const Version1 = 1
  5. export const Version = Version1
  6. export const PriceStatus = ['Unknown', 'Trading', 'Halted', 'Auction']
  7. export const CorpAction = ['NoCorpAct']
  8. export const PriceType = ['Unknown', 'Price', 'TWAP', 'Volatility']
  9. const empty32Buffer = Buffer.alloc(32)
  10. const PKorNull = (data: Buffer) => (data.equals(empty32Buffer) ? null : new web3.PublicKey(data))
  11. interface ICreatePriceFeed {
  12. oracleProgram: Program
  13. initPrice: number
  14. confidence?: BN
  15. expo?: number
  16. }
  17. export const createPriceFeed = async ({
  18. oracleProgram,
  19. initPrice,
  20. confidence,
  21. expo = -4,
  22. }: ICreatePriceFeed) => {
  23. const conf = confidence || new BN((initPrice / 10) * 10 ** -expo)
  24. const collateralTokenFeed = new web3.Account()
  25. await oracleProgram.rpc.initialize(new BN(initPrice * 10 ** -expo), expo, conf, {
  26. accounts: { price: collateralTokenFeed.publicKey },
  27. signers: [collateralTokenFeed],
  28. instructions: [
  29. web3.SystemProgram.createAccount({
  30. fromPubkey: oracleProgram.provider.wallet.publicKey,
  31. newAccountPubkey: collateralTokenFeed.publicKey,
  32. space: 3312,
  33. lamports: await oracleProgram.provider.connection.getMinimumBalanceForRentExemption(3312),
  34. programId: oracleProgram.programId,
  35. }),
  36. ],
  37. })
  38. return collateralTokenFeed.publicKey
  39. }
  40. export const setFeedPrice = async (
  41. oracleProgram: Program,
  42. newPrice: number,
  43. priceFeed: web3.PublicKey
  44. ) => {
  45. const info = await oracleProgram.provider.connection.getAccountInfo(priceFeed)
  46. const data = parsePriceData(info.data)
  47. await oracleProgram.rpc.setPrice(new BN(newPrice * 10 ** -data.exponent), {
  48. accounts: { price: priceFeed },
  49. })
  50. }
  51. export const getFeedData = async (oracleProgram: Program, priceFeed: web3.PublicKey) => {
  52. const info = await oracleProgram.provider.connection.getAccountInfo(priceFeed)
  53. return parsePriceData(info.data)
  54. }
  55. // https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L758
  56. const ERR_BUFFER_OUT_OF_BOUNDS = () => new Error('Attempt to access memory outside buffer bounds')
  57. // https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L968
  58. const ERR_INVALID_ARG_TYPE = (name: string, expected: string, actual: any) =>
  59. new Error(`The "${name}" argument must be of type ${expected}. Received ${actual}`)
  60. // https://github.com/nodejs/node/blob/v14.17.0/lib/internal/errors.js#L1262
  61. const ERR_OUT_OF_RANGE = (str: string, range: string, received: number) =>
  62. new Error(`The value of "${str} is out of range. It must be ${range}. Received ${received}`)
  63. // https://github.com/nodejs/node/blob/v14.17.0/lib/internal/validators.js#L127-L130
  64. function validateNumber(value: any, name: string) {
  65. if (typeof value !== 'number') throw ERR_INVALID_ARG_TYPE(name, 'number', value)
  66. }
  67. // https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L68-L80
  68. function boundsError(value: number, length: number) {
  69. if (Math.floor(value) !== value) {
  70. validateNumber(value, 'offset')
  71. throw ERR_OUT_OF_RANGE('offset', 'an integer', value)
  72. }
  73. if (length < 0) throw ERR_BUFFER_OUT_OF_BOUNDS()
  74. throw ERR_OUT_OF_RANGE('offset', `>= 0 and <= ${length}`, value)
  75. }
  76. export function readBigInt64LE(buffer: Buffer, offset = 0): bigint {
  77. validateNumber(offset, 'offset')
  78. const first = buffer[offset]
  79. const last = buffer[offset + 7]
  80. if (first === undefined || last === undefined) boundsError(offset, buffer.length - 8)
  81. const val =
  82. buffer[offset + 4] + buffer[offset + 5] * 2 ** 8 + buffer[offset + 6] * 2 ** 16 + (last << 24) // Overflow
  83. return (
  84. (BigInt(val) << BigInt(32)) +
  85. BigInt(
  86. first + buffer[++offset] * 2 ** 8 + buffer[++offset] * 2 ** 16 + buffer[++offset] * 2 ** 24
  87. )
  88. )
  89. }
  90. // https://github.com/nodejs/node/blob/v14.17.0/lib/internal/buffer.js#L89-L107
  91. export function readBigUInt64LE(buffer: Buffer, offset = 0): bigint {
  92. validateNumber(offset, 'offset')
  93. const first = buffer[offset]
  94. const last = buffer[offset + 7]
  95. if (first === undefined || last === undefined) boundsError(offset, buffer.length - 8)
  96. const lo =
  97. first + buffer[++offset] * 2 ** 8 + buffer[++offset] * 2 ** 16 + buffer[++offset] * 2 ** 24
  98. const hi =
  99. buffer[++offset] + buffer[++offset] * 2 ** 8 + buffer[++offset] * 2 ** 16 + last * 2 ** 24
  100. return BigInt(lo) + (BigInt(hi) << BigInt(32)) // tslint:disable-line:no-bitwise
  101. }
  102. export const parsePriceData = (data: Buffer) => {
  103. // Pyth magic number.
  104. const magic = data.readUInt32LE(0)
  105. // Program version.
  106. const version = data.readUInt32LE(4)
  107. // Account type.
  108. const type = data.readUInt32LE(8)
  109. // Price account size.
  110. const size = data.readUInt32LE(12)
  111. // Price or calculation type.
  112. const priceType = data.readUInt32LE(16)
  113. // Price exponent.
  114. const exponent = data.readInt32LE(20)
  115. // Number of component prices.
  116. const numComponentPrices = data.readUInt32LE(24)
  117. // unused
  118. // const unused = accountInfo.data.readUInt32LE(28)
  119. // Currently accumulating price slot.
  120. const currentSlot = readBigUInt64LE(data, 32)
  121. // Valid on-chain slot of aggregate price.
  122. const validSlot = readBigUInt64LE(data, 40)
  123. // Time-weighted average price.
  124. const twapComponent = readBigInt64LE(data, 48)
  125. const twap = Number(twapComponent) * 10 ** exponent
  126. // Annualized price volatility.
  127. const avolComponent = readBigUInt64LE(data, 56)
  128. const avol = Number(avolComponent) * 10 ** exponent
  129. // Space for future derived values.
  130. const drv0Component = readBigInt64LE(data, 64)
  131. const drv0 = Number(drv0Component) * 10 ** exponent
  132. const drv1Component = readBigInt64LE(data, 72)
  133. const drv1 = Number(drv1Component) * 10 ** exponent
  134. const drv2Component = readBigInt64LE(data, 80)
  135. const drv2 = Number(drv2Component) * 10 ** exponent
  136. const drv3Component = readBigInt64LE(data, 88)
  137. const drv3 = Number(drv3Component) * 10 ** exponent
  138. const drv4Component = readBigInt64LE(data, 96)
  139. const drv4 = Number(drv4Component) * 10 ** exponent
  140. const drv5Component = readBigInt64LE(data, 104)
  141. const drv5 = Number(drv5Component) * 10 ** exponent
  142. // Product id / reference account.
  143. const productAccountKey = new web3.PublicKey(data.slice(112, 144))
  144. // Next price account in list.
  145. const nextPriceAccountKey = PKorNull(data.slice(144, 176))
  146. // Aggregate price updater.
  147. const aggregatePriceUpdaterAccountKey = new web3.PublicKey(data.slice(176, 208))
  148. const aggregatePriceInfo = parsePriceInfo(data.slice(208, 240), exponent)
  149. // Price components - up to 32.
  150. const priceComponents = []
  151. let offset = 240
  152. let shouldContinue = true
  153. while (offset < data.length && shouldContinue) {
  154. const publisher = PKorNull(data.slice(offset, offset + 32))
  155. offset += 32
  156. if (publisher) {
  157. const aggregate = parsePriceInfo(data.slice(offset, offset + 32), exponent)
  158. offset += 32
  159. const latest = parsePriceInfo(data.slice(offset, offset + 32), exponent)
  160. offset += 32
  161. priceComponents.push({ publisher, aggregate, latest })
  162. } else {
  163. shouldContinue = false
  164. }
  165. }
  166. return {
  167. magic,
  168. version,
  169. type,
  170. size,
  171. priceType,
  172. exponent,
  173. numComponentPrices,
  174. currentSlot,
  175. validSlot,
  176. twapComponent,
  177. twap,
  178. avolComponent,
  179. avol,
  180. drv0Component,
  181. drv0,
  182. drv1Component,
  183. drv1,
  184. drv2Component,
  185. drv2,
  186. drv3Component,
  187. drv3,
  188. drv4Component,
  189. drv4,
  190. drv5Component,
  191. drv5,
  192. productAccountKey,
  193. nextPriceAccountKey,
  194. aggregatePriceUpdaterAccountKey,
  195. ...aggregatePriceInfo,
  196. priceComponents,
  197. }
  198. }
  199. interface ProductAttributes {
  200. [index: string]: string
  201. }
  202. export const parseProductData = (data: Buffer) => {
  203. // Pyth magic number.
  204. const magic = data.readUInt32LE(0)
  205. // Program version.
  206. const version = data.readUInt32LE(4)
  207. // Account type.
  208. const type = data.readUInt32LE(8)
  209. // Price account size.
  210. const size = data.readUInt32LE(12)
  211. // First price account in list.
  212. const priceAccountBytes = data.slice(16, 48)
  213. const priceAccountKey = new web3.PublicKey(priceAccountBytes)
  214. const product: ProductAttributes = {}
  215. let idx = 48
  216. while (idx < data.length) {
  217. const keyLength = data[idx]
  218. idx++
  219. if (keyLength) {
  220. const key = data.slice(idx, idx + keyLength).toString()
  221. idx += keyLength
  222. const valueLength = data[idx]
  223. idx++
  224. const value = data.slice(idx, idx + valueLength).toString()
  225. idx += valueLength
  226. product[key] = value
  227. }
  228. }
  229. return { magic, version, type, size, priceAccountKey, product }
  230. }
  231. const parsePriceInfo = (data: Buffer, exponent: number) => {
  232. // Aggregate price.
  233. const priceComponent = data.readBigUInt64LE(0)
  234. const price = Number(priceComponent) * 10 ** exponent
  235. // Aggregate confidence.
  236. const confidenceComponent = data.readBigUInt64LE(8)
  237. const confidence = Number(confidenceComponent) * 10 ** exponent
  238. // Aggregate status.
  239. const status = data.readUInt32LE(16)
  240. // Aggregate corporate action.
  241. const corporateAction = data.readUInt32LE(20)
  242. // Aggregate publish slot.
  243. const publishSlot = data.readBigUInt64LE(24)
  244. return {
  245. priceComponent,
  246. price,
  247. confidenceComponent,
  248. confidence,
  249. status,
  250. corporateAction,
  251. publishSlot,
  252. }
  253. }