UNPKG

@mysten/suins

Version:
219 lines (217 loc) 8.02 kB
import { PriceServiceConnection } from "./PriceServiceConnection.mjs"; import { extractVaaBytesFromAccumulatorMessage } from "./pyth-helpers.mjs"; import { State } from "../contracts/pyth/state.mjs"; import { State as State$1 } from "../contracts/wormhole/state.mjs"; import { fromBase64, fromHex, parseStructTag } from "@mysten/sui/utils"; import { bcs } from "@mysten/sui/bcs"; import { coinWithBalance } from "@mysten/sui/transactions"; //#region src/pyth/pyth.ts const MAX_ARGUMENT_SIZE = 16 * 1024; var SuiPriceServiceConnection = class extends PriceServiceConnection { /** * Fetch price feed update data. * * @param priceIds Array of hex-encoded price IDs. * @returns Array of buffers containing the price update data. */ async getPriceFeedsUpdateData(priceIds) { return (await this.getLatestVaas(priceIds)).map((vaa) => fromBase64(vaa)); } }; var SuiPythClient = class { #pythState; #wormholePackageId; #priceFeedObjectIdCache = /* @__PURE__ */ new Map(); #priceTableInfo; constructor(provider, pythStateId, wormholeStateId) { this.provider = provider; this.pythStateId = pythStateId; this.wormholeStateId = wormholeStateId; } /** * Verifies the VAAs using the Wormhole contract. * * @param vaas Array of VAA buffers to verify. * @param tx Transaction block to add commands to. * @returns Array of verified VAAs. */ 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.vector("u8", vaa), tx.object.clock() ] }); verifiedVaas.push(verifiedVaa); } return verifiedVaas; } /** * 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). * @param feeCoin Optional custom SUI coin to use for Pyth oracle fees. If not provided, uses gas coin. */ async updatePriceFeeds(tx, updates, feedIds, feeCoin) { const packageId = await this.getPythPackageId(); let priceUpdatesHotPotato; if (updates.length > 1) throw new Error("SDK does not support sending multiple accumulator messages in a single transaction"); const vaa = extractVaaBytesFromAccumulatorMessage(updates[0]); const verifiedVaas = await this.verifyVaas([vaa], tx); [priceUpdatesHotPotato] = tx.moveCall({ target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`, arguments: [ tx.object(this.pythStateId), tx.pure(bcs.vector(bcs.U8).serialize(Array.from(updates[0]), { maxSize: MAX_ARGUMENT_SIZE }).toBytes()), verifiedVaas[0], tx.object.clock() ] }); const priceInfoObjects = []; const baseUpdateFee = await this.getBaseUpdateFee(); 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); const feePayment = feeCoin ? tx.splitCoins(feeCoin, [tx.pure.u64(baseUpdateFee)])[0] : coinWithBalance({ balance: baseUpdateFee }); [priceUpdatesHotPotato] = tx.moveCall({ target: `${packageId}::pyth::update_single_price_feed`, arguments: [ tx.object(this.pythStateId), priceUpdatesHotPotato, tx.object(priceInfoObjectId), feePayment, tx.object.clock() ] }); } tx.moveCall({ target: `${packageId}::hot_potato_vector::destroy`, arguments: [priceUpdatesHotPotato], typeArguments: [`${packageId}::price_info::PriceInfo`] }); return priceInfoObjects; } /** * Get the price feed object ID for a given feed ID, caching the promise. * @param feedId */ getPriceFeedObjectId(feedId) { if (!this.#priceFeedObjectIdCache.has(feedId)) this.#priceFeedObjectIdCache.set(feedId, this.#fetchPriceFeedObjectId(feedId).catch((err) => { this.#priceFeedObjectIdCache.delete(feedId); throw err; })); return this.#priceFeedObjectIdCache.get(feedId); } /** * Fetches the price feed object ID for a given feed ID (no caching). * Throws an error if the object is not found. */ async #fetchPriceFeedObjectId(feedId) { const { id: tableId, fieldType } = await this.getPriceTableInfo(); const result = await this.provider.core.getDynamicField({ parentId: tableId, name: { type: `${fieldType}::price_identifier::PriceIdentifier`, bcs: bcs.byteVector().serialize(fromHex(feedId)).toBytes() } }); if (!result.dynamicField) throw new Error(`Price feed object ID for feed ID ${feedId} not found.`); return bcs.Address.parse(result.dynamicField.value.bcs); } /** * Fetches the price table object ID for the current state ID, caching the promise. * @returns Price table object ID and field type */ getPriceTableInfo() { if (!this.#priceTableInfo) this.#priceTableInfo = this.#fetchPriceTableInfo().catch((err) => { this.#priceTableInfo = void 0; throw err; }); return this.#priceTableInfo; } /** * Fetches the price table object ID and field type (no caching). * @returns Price table object ID and field type */ async #fetchPriceTableInfo() { const result = await this.provider.core.getDynamicObjectField({ parentId: this.pythStateId, name: { type: "vector<u8>", bcs: bcs.string().serialize("price_info").toBytes() } }); if (!result.object) throw new Error("Price Table not found, contract may not be initialized"); const priceIdentifier = parseStructTag(result.object.type).typeParams[0]; if (typeof priceIdentifier === "object" && priceIdentifier !== null && priceIdentifier.name === "PriceIdentifier" && "address" in priceIdentifier) return { id: result.object.objectId, fieldType: priceIdentifier.address }; else throw new Error("fieldType not found"); } /** * Fetches the package ID for the Wormhole contract, with caching. */ getWormholePackageId() { if (!this.#wormholePackageId) this.#wormholePackageId = this.#fetchWormholePackageId(); return this.#wormholePackageId; } /** * Fetches the package ID for the Wormhole contract (no caching). */ async #fetchWormholePackageId() { var _result$object; const result = await this.provider.core.getObject({ objectId: this.wormholeStateId, include: { content: true } }); if (!((_result$object = result.object) === null || _result$object === void 0 ? void 0 : _result$object.content)) throw new Error("Unable to fetch Wormhole state object"); return State$1.parse(result.object.content).upgrade_cap.package; } /** * Fetches and caches the parsed Pyth state object. * This is shared between getPythPackageId and getBaseUpdateFee to avoid redundant fetches. */ #getPythState() { if (!this.#pythState) this.#pythState = this.#fetchPythState(); return this.#pythState; } /** * Fetches the Pyth state object (no caching). */ async #fetchPythState() { var _result$object2; const result = await this.provider.core.getObject({ objectId: this.pythStateId, include: { content: true } }); if (!((_result$object2 = result.object) === null || _result$object2 === void 0 ? void 0 : _result$object2.content)) throw new Error("Unable to fetch Pyth state object"); return State.parse(result.object.content); } /** * Fetches the package ID for the Pyth contract, with caching. * Uses the shared Pyth state cache. */ async getPythPackageId() { return (await this.#getPythState()).upgrade_cap.package; } /** * Returns the cached base update fee, fetching it if necessary. * Uses the shared Pyth state cache. */ async getBaseUpdateFee() { const state = await this.#getPythState(); return Number(state.base_update_fee); } }; //#endregion export { SuiPriceServiceConnection, SuiPythClient }; //# sourceMappingURL=pyth.mjs.map