client.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { IotaClient } from "@iota/iota-sdk/client";
  2. import { IOTA_CLOCK_OBJECT_ID } from "@iota/iota-sdk/utils";
  3. import { Transaction } from "@iota/iota-sdk/transactions";
  4. import { bcs } from "@iota/iota-sdk/bcs";
  5. import { type HexString } from "@pythnetwork/price-service-client";
  6. import { Buffer } from "buffer";
  7. const MAX_ARGUMENT_SIZE = 16 * 1024;
  8. export type ObjectId = string;
  9. export class IotaPythClient {
  10. private pythPackageId: ObjectId | undefined;
  11. private wormholePackageId: ObjectId | undefined;
  12. private priceTableInfo: { id: ObjectId; fieldType: ObjectId } | undefined;
  13. private priceFeedObjectIdCache: Map<HexString, ObjectId> = new Map();
  14. private baseUpdateFee: number | undefined;
  15. constructor(
  16. public provider: IotaClient,
  17. public pythStateId: ObjectId,
  18. public wormholeStateId: ObjectId,
  19. ) {
  20. this.pythPackageId = undefined;
  21. this.wormholePackageId = undefined;
  22. }
  23. async getBaseUpdateFee(): Promise<number> {
  24. if (this.baseUpdateFee === undefined) {
  25. const result = await this.provider.getObject({
  26. id: this.pythStateId,
  27. options: { showContent: true },
  28. });
  29. if (
  30. !result.data ||
  31. !result.data.content ||
  32. result.data.content.dataType !== "moveObject"
  33. )
  34. throw new Error("Unable to fetch pyth state object");
  35. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  36. // @ts-ignore
  37. this.baseUpdateFee = result.data.content.fields.base_update_fee as number;
  38. }
  39. return this.baseUpdateFee;
  40. }
  41. /**
  42. * getPackageId returns the latest package id that the object belongs to. Use this to
  43. * fetch the latest package id for a given object id and handle package upgrades automatically.
  44. * @param objectId
  45. * @returns package id
  46. */
  47. async getPackageId(objectId: ObjectId): Promise<ObjectId> {
  48. const state = await this.provider
  49. .getObject({
  50. id: objectId,
  51. options: {
  52. showContent: true,
  53. },
  54. })
  55. .then((result) => {
  56. if (result.data?.content?.dataType == "moveObject") {
  57. return result.data.content.fields;
  58. }
  59. console.log(result.data?.content);
  60. throw new Error(`Cannot fetch package id for object ${objectId}`);
  61. });
  62. if ("upgrade_cap" in state) {
  63. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  64. // @ts-ignore
  65. return state.upgrade_cap.fields.package;
  66. }
  67. throw new Error("upgrade_cap not found");
  68. }
  69. /**
  70. * Adds the commands for calling wormhole and verifying the vaas and returns the verified vaas.
  71. * @param vaas array of vaas to verify
  72. * @param tx transaction block to add commands to
  73. */
  74. async verifyVaas(vaas: Buffer[], tx: Transaction) {
  75. const wormholePackageId = await this.getWormholePackageId();
  76. const verifiedVaas = [];
  77. for (const vaa of vaas) {
  78. const [verifiedVaa] = tx.moveCall({
  79. target: `${wormholePackageId}::vaa::parse_and_verify`,
  80. arguments: [
  81. tx.object(this.wormholeStateId),
  82. tx.pure(
  83. bcs
  84. .vector(bcs.U8)
  85. .serialize(Array.from(vaa), {
  86. maxSize: MAX_ARGUMENT_SIZE,
  87. })
  88. .toBytes(),
  89. ),
  90. tx.object(IOTA_CLOCK_OBJECT_ID),
  91. ],
  92. });
  93. verifiedVaas.push(verifiedVaa);
  94. }
  95. return verifiedVaas;
  96. }
  97. /**
  98. * Adds the necessary commands for updating the pyth price feeds to the transaction block.
  99. * @param tx transaction block to add commands to
  100. * @param updates array of price feed updates received from the price service
  101. * @param feedIds array of feed ids to update (in hex format)
  102. */
  103. async updatePriceFeeds(
  104. tx: Transaction,
  105. updates: Buffer[],
  106. feedIds: HexString[],
  107. ): Promise<ObjectId[]> {
  108. const packageId = await this.getPythPackageId();
  109. let priceUpdatesHotPotato;
  110. if (updates.length > 1) {
  111. throw new Error(
  112. "SDK does not support sending multiple accumulator messages in a single transaction",
  113. );
  114. }
  115. const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]!);
  116. const verifiedVaas = await this.verifyVaas([vaa], tx);
  117. [priceUpdatesHotPotato] = tx.moveCall({
  118. target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`,
  119. arguments: [
  120. tx.object(this.pythStateId),
  121. tx.pure(
  122. bcs
  123. .vector(bcs.U8)
  124. .serialize(Array.from(updates[0]!), {
  125. maxSize: MAX_ARGUMENT_SIZE,
  126. })
  127. .toBytes(),
  128. ),
  129. verifiedVaas[0]!,
  130. tx.object(IOTA_CLOCK_OBJECT_ID),
  131. ],
  132. });
  133. const priceInfoObjects: ObjectId[] = [];
  134. const baseUpdateFee = await this.getBaseUpdateFee();
  135. const coins = tx.splitCoins(
  136. tx.gas,
  137. feedIds.map(() => tx.pure.u64(baseUpdateFee)),
  138. );
  139. let coinId = 0;
  140. for (const feedId of feedIds) {
  141. const priceInfoObjectId = await this.getPriceFeedObjectId(feedId);
  142. if (!priceInfoObjectId) {
  143. throw new Error(
  144. `Price feed ${feedId} not found, please create it first`,
  145. );
  146. }
  147. priceInfoObjects.push(priceInfoObjectId);
  148. [priceUpdatesHotPotato] = tx.moveCall({
  149. target: `${packageId}::pyth::update_single_price_feed`,
  150. arguments: [
  151. tx.object(this.pythStateId),
  152. priceUpdatesHotPotato!,
  153. tx.object(priceInfoObjectId),
  154. coins[coinId]!,
  155. tx.object(IOTA_CLOCK_OBJECT_ID),
  156. ],
  157. });
  158. coinId++;
  159. }
  160. tx.moveCall({
  161. target: `${packageId}::hot_potato_vector::destroy`,
  162. arguments: [priceUpdatesHotPotato!],
  163. typeArguments: [`${packageId}::price_info::PriceInfo`],
  164. });
  165. return priceInfoObjects;
  166. }
  167. async createPriceFeed(tx: Transaction, updates: Buffer[]) {
  168. const packageId = await this.getPythPackageId();
  169. if (updates.length > 1) {
  170. throw new Error(
  171. "SDK does not support sending multiple accumulator messages in a single transaction",
  172. );
  173. }
  174. const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]!);
  175. const verifiedVaas = await this.verifyVaas([vaa], tx);
  176. tx.moveCall({
  177. target: `${packageId}::pyth::create_price_feeds_using_accumulator`,
  178. arguments: [
  179. tx.object(this.pythStateId),
  180. tx.pure(
  181. bcs
  182. .vector(bcs.U8)
  183. .serialize(Array.from(updates[0]!), {
  184. maxSize: MAX_ARGUMENT_SIZE,
  185. })
  186. .toBytes(),
  187. ),
  188. verifiedVaas[0]!,
  189. tx.object(IOTA_CLOCK_OBJECT_ID),
  190. ],
  191. });
  192. }
  193. /**
  194. * Get the packageId for the wormhole package if not already cached
  195. */
  196. async getWormholePackageId() {
  197. if (!this.wormholePackageId) {
  198. this.wormholePackageId = await this.getPackageId(this.wormholeStateId);
  199. }
  200. return this.wormholePackageId;
  201. }
  202. /**
  203. * Get the packageId for the pyth package if not already cached
  204. */
  205. async getPythPackageId() {
  206. if (!this.pythPackageId) {
  207. this.pythPackageId = await this.getPackageId(this.pythStateId);
  208. }
  209. return this.pythPackageId;
  210. }
  211. /**
  212. * Get the priceFeedObjectId for a given feedId if not already cached
  213. * @param feedId
  214. */
  215. async getPriceFeedObjectId(feedId: HexString): Promise<ObjectId | undefined> {
  216. const normalizedFeedId = feedId.replace("0x", "");
  217. if (!this.priceFeedObjectIdCache.has(normalizedFeedId)) {
  218. const { id: tableId, fieldType } = await this.getPriceTableInfo();
  219. const result = await this.provider.getDynamicFieldObject({
  220. parentId: tableId,
  221. name: {
  222. type: `${fieldType}::price_identifier::PriceIdentifier`,
  223. value: {
  224. bytes: Array.from(Buffer.from(normalizedFeedId, "hex")),
  225. },
  226. },
  227. });
  228. if (!result.data || !result.data.content) {
  229. return undefined;
  230. }
  231. if (result.data.content.dataType !== "moveObject") {
  232. throw new Error("Price feed type mismatch");
  233. }
  234. this.priceFeedObjectIdCache.set(
  235. normalizedFeedId,
  236. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  237. // @ts-ignore
  238. result.data.content.fields.value,
  239. );
  240. }
  241. return this.priceFeedObjectIdCache.get(normalizedFeedId);
  242. }
  243. /**
  244. * Fetches the price table object id for the current state id if not cached
  245. * @returns price table object id
  246. */
  247. async getPriceTableInfo(): Promise<{ id: ObjectId; fieldType: ObjectId }> {
  248. if (this.priceTableInfo === undefined) {
  249. const result = await this.provider.getDynamicFieldObject({
  250. parentId: this.pythStateId,
  251. name: {
  252. type: "vector<u8>",
  253. value: "price_info",
  254. },
  255. });
  256. if (!result.data || !result.data.type) {
  257. throw new Error(
  258. "Price Table not found, contract may not be initialized",
  259. );
  260. }
  261. let type = result.data.type.replace("0x2::table::Table<", "");
  262. type = type.replace(
  263. "::price_identifier::PriceIdentifier, 0x2::object::ID>",
  264. "",
  265. );
  266. this.priceTableInfo = { id: result.data.objectId, fieldType: type };
  267. }
  268. return this.priceTableInfo;
  269. }
  270. /**
  271. * Obtains the vaa bytes embedded in an accumulator message.
  272. * @param accumulatorMessage - the accumulator price update message
  273. * @returns vaa bytes as a uint8 array
  274. */
  275. extractVaaBytesFromAccumulatorMessage(accumulatorMessage: Buffer): Buffer {
  276. // the first 6 bytes in the accumulator message encode the header, major, and minor bytes
  277. // we ignore them, since we are only interested in the VAA bytes
  278. const trailingPayloadSize = accumulatorMessage.readUint8(6);
  279. const vaaSizeOffset =
  280. 7 + // header bytes (header(4) + major(1) + minor(1) + trailing payload size(1))
  281. trailingPayloadSize + // trailing payload (variable number of bytes)
  282. 1; // proof_type (1 byte)
  283. const vaaSize = accumulatorMessage.readUint16BE(vaaSizeOffset);
  284. const vaaOffset = vaaSizeOffset + 2;
  285. return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize);
  286. }
  287. }