UNPKG

@pythnetwork/pyth-sui-js

Version:
279 lines (278 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SuiPythClient = void 0; const utils_1 = require("@mysten/sui/utils"); const bcs_1 = require("@mysten/sui/bcs"); const buffer_1 = require("buffer"); const MAX_ARGUMENT_SIZE = 16 * 1024; class SuiPythClient { provider; pythStateId; wormholeStateId; pythPackageId; wormholePackageId; priceTableInfo; priceFeedObjectIdCache = new Map(); baseUpdateFee; constructor(provider, pythStateId, wormholeStateId) { this.provider = provider; this.pythStateId = pythStateId; this.wormholeStateId = wormholeStateId; this.pythPackageId = undefined; this.wormholePackageId = undefined; } async getBaseUpdateFee() { if (this.baseUpdateFee === undefined) { const result = await this.provider.getObject({ id: this.pythStateId, options: { showContent: true }, }); if (!result.data || !result.data.content || result.data.content.dataType !== "moveObject") throw new Error("Unable to fetch pyth state object"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.baseUpdateFee = result.data.content.fields.base_update_fee; } return this.baseUpdateFee; } /** * getPackageId returns the latest package id that the object belongs to. Use this to * fetch the latest package id for a given object id and handle package upgrades automatically. * @param objectId * @returns package id */ async getPackageId(objectId) { const state = await this.provider .getObject({ id: objectId, options: { showContent: true, }, }) .then((result) => { if (result.data?.content?.dataType == "moveObject") { return result.data.content.fields; } console.log(result.data?.content); throw new Error(`Cannot fetch package id for object ${objectId}`); }); if ("upgrade_cap" in state) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return state.upgrade_cap.fields.package; } throw new Error("upgrade_cap not found"); } /** * Adds the commands for calling wormhole and verifying the vaas and returns the verified vaas. * @param vaas array of vaas to verify * @param tx transaction block to add commands to */ async verifyVaas(vaas, tx) { const wormholePackageId = await this.getWormholePackageId(); const verifiedVaas = []; for (const vaa of vaas) { const [verifiedVaa] = tx.moveCall({ target: `${wormholePackageId}::vaa::parse_and_verify`, arguments: [ tx.object(this.wormholeStateId), tx.pure(bcs_1.bcs .vector(bcs_1.bcs.U8) .serialize(Array.from(vaa), { maxSize: MAX_ARGUMENT_SIZE, }) .toBytes()), tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); verifiedVaas.push(verifiedVaa); } return verifiedVaas; } async verifyVaasAndGetHotPotato(tx, updates, packageId) { if (updates.length > 1) { throw new Error("SDK does not support sending multiple accumulator messages in a single transaction"); } const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]); const verifiedVaas = await this.verifyVaas([vaa], tx); const [priceUpdatesHotPotato] = tx.moveCall({ target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`, arguments: [ tx.object(this.pythStateId), tx.pure(bcs_1.bcs .vector(bcs_1.bcs.U8) .serialize(Array.from(updates[0]), { maxSize: MAX_ARGUMENT_SIZE, }) .toBytes()), verifiedVaas[0], tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); return priceUpdatesHotPotato; } async executePriceFeedUpdates(tx, packageId, feedIds, priceUpdatesHotPotato, coins) { const priceInfoObjects = []; let coinId = 0; for (const feedId of feedIds) { const priceInfoObjectId = await this.getPriceFeedObjectId(feedId); if (!priceInfoObjectId) { throw new Error(`Price feed ${feedId} not found, please create it first`); } priceInfoObjects.push(priceInfoObjectId); [priceUpdatesHotPotato] = tx.moveCall({ target: `${packageId}::pyth::update_single_price_feed`, arguments: [ tx.object(this.pythStateId), priceUpdatesHotPotato, tx.object(priceInfoObjectId), coins[coinId], tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); coinId++; } tx.moveCall({ target: `${packageId}::hot_potato_vector::destroy`, arguments: [priceUpdatesHotPotato], typeArguments: [`${packageId}::price_info::PriceInfo`], }); return priceInfoObjects; } /** * Adds the necessary commands for updating the pyth price feeds to the transaction block. * @param tx transaction block to add commands to * @param updates array of price feed updates received from the price service * @param feedIds array of feed ids to update (in hex format) */ async updatePriceFeeds(tx, updates, feedIds) { const packageId = await this.getPythPackageId(); const priceUpdatesHotPotato = await this.verifyVaasAndGetHotPotato(tx, updates, packageId); const baseUpdateFee = await this.getBaseUpdateFee(); const coins = tx.splitCoins(tx.gas, feedIds.map(() => tx.pure.u64(baseUpdateFee))); return await this.executePriceFeedUpdates(tx, packageId, feedIds, priceUpdatesHotPotato, coins); } /** * Updates price feeds using the coin input for payment. Coins can be generated by calling splitCoin on tx.gas. * @param tx transaction block to add commands to * @param updates array of price feed updates received from the price service * @param feedIds array of feed ids to update (in hex format) * @param coins array of Coins for payment of update operations */ async updatePriceFeedsWithCoins(tx, updates, feedIds, coins) { const packageId = await this.getPythPackageId(); const priceUpdatesHotPotato = await this.verifyVaasAndGetHotPotato(tx, updates, packageId); return await this.executePriceFeedUpdates(tx, packageId, feedIds, priceUpdatesHotPotato, coins); } async createPriceFeed(tx, updates) { const packageId = await this.getPythPackageId(); if (updates.length > 1) { throw new Error("SDK does not support sending multiple accumulator messages in a single transaction"); } const vaa = this.extractVaaBytesFromAccumulatorMessage(updates[0]); const verifiedVaas = await this.verifyVaas([vaa], tx); tx.moveCall({ target: `${packageId}::pyth::create_price_feeds_using_accumulator`, arguments: [ tx.object(this.pythStateId), tx.pure(bcs_1.bcs .vector(bcs_1.bcs.U8) .serialize(Array.from(updates[0]), { maxSize: MAX_ARGUMENT_SIZE, }) .toBytes()), verifiedVaas[0], tx.object(utils_1.SUI_CLOCK_OBJECT_ID), ], }); } /** * Get the packageId for the wormhole package if not already cached */ async getWormholePackageId() { if (!this.wormholePackageId) { this.wormholePackageId = await this.getPackageId(this.wormholeStateId); } return this.wormholePackageId; } /** * Get the packageId for the pyth package if not already cached */ async getPythPackageId() { if (!this.pythPackageId) { this.pythPackageId = await this.getPackageId(this.pythStateId); } return this.pythPackageId; } /** * Get the priceFeedObjectId for a given feedId if not already cached * @param feedId */ async getPriceFeedObjectId(feedId) { const normalizedFeedId = feedId.replace("0x", ""); if (!this.priceFeedObjectIdCache.has(normalizedFeedId)) { const { id: tableId, fieldType } = await this.getPriceTableInfo(); const result = await this.provider.getDynamicFieldObject({ parentId: tableId, name: { type: `${fieldType}::price_identifier::PriceIdentifier`, value: { bytes: Array.from(buffer_1.Buffer.from(normalizedFeedId, "hex")), }, }, }); if (!result.data || !result.data.content) { return undefined; } if (result.data.content.dataType !== "moveObject") { throw new Error("Price feed type mismatch"); } this.priceFeedObjectIdCache.set(normalizedFeedId, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore result.data.content.fields.value); } return this.priceFeedObjectIdCache.get(normalizedFeedId); } /** * Fetches the price table object id for the current state id if not cached * @returns price table object id */ async getPriceTableInfo() { if (this.priceTableInfo === undefined) { const result = await this.provider.getDynamicFieldObject({ parentId: this.pythStateId, name: { type: "vector<u8>", value: "price_info", }, }); if (!result.data || !result.data.type) { throw new Error("Price Table not found, contract may not be initialized"); } let type = result.data.type.replace("0x2::table::Table<", ""); type = type.replace("::price_identifier::PriceIdentifier, 0x2::object::ID>", ""); this.priceTableInfo = { id: result.data.objectId, fieldType: type }; } return this.priceTableInfo; } /** * Obtains the vaa bytes embedded in an accumulator message. * @param accumulatorMessage - the accumulator price update message * @returns vaa bytes as a uint8 array */ extractVaaBytesFromAccumulatorMessage(accumulatorMessage) { // the first 6 bytes in the accumulator message encode the header, major, and minor bytes // we ignore them, since we are only interested in the VAA bytes const trailingPayloadSize = accumulatorMessage.readUint8(6); const vaaSizeOffset = 7 + // header bytes (header(4) + major(1) + minor(1) + trailing payload size(1)) trailingPayloadSize + // trailing payload (variable number of bytes) 1; // proof_type (1 byte) const vaaSize = accumulatorMessage.readUint16BE(vaaSizeOffset); const vaaOffset = vaaSizeOffset + 2; return accumulatorMessage.subarray(vaaOffset, vaaOffset + vaaSize); } } exports.SuiPythClient = SuiPythClient;