| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- import {
- builder,
- JsonRpcProvider,
- ObjectId,
- SUI_CLOCK_OBJECT_ID,
- TransactionBlock,
- } from "@mysten/sui.js";
- import { HexString } from "@pythnetwork/price-service-client";
- export class SuiPythClient {
- private pythPackageId: ObjectId | undefined;
- private wormholePackageId: ObjectId | undefined;
- private priceTableId: ObjectId | undefined;
- private priceFeedObjectIdCache: Map<HexString, ObjectId> = new Map();
- private baseUpdateFee: number | undefined;
- constructor(
- public provider: JsonRpcProvider,
- public pythStateId: ObjectId,
- public wormholeStateId: ObjectId
- ) {
- this.pythPackageId = undefined;
- this.wormholePackageId = undefined;
- }
- async getBaseUpdateFee(): Promise<number> {
- 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");
- this.baseUpdateFee = result.data.content.fields.base_update_fee as number;
- }
- 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: ObjectId): Promise<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;
- }
- throw new Error("not move object");
- });
- if ("upgrade_cap" in state) {
- 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: Buffer[], tx: TransactionBlock) {
- 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(Array.from(vaa)),
- tx.object(SUI_CLOCK_OBJECT_ID),
- ],
- });
- 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)
- */
- async updatePriceFeeds(
- tx: TransactionBlock,
- updates: Buffer[],
- feedIds: HexString[]
- ): Promise<ObjectId[]> {
- const wormholePackageId = await this.getWormholePackageId();
- const packageId = await this.getPythPackageId();
- let priceUpdatesHotPotato;
- if (updates.every((update) => this.isAccumulatorMsg(update))) {
- 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);
- [priceUpdatesHotPotato] = tx.moveCall({
- target: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`,
- arguments: [
- tx.object(this.pythStateId),
- tx.pure(
- builder
- .ser("vector<u8>", Array.from(updates[0]), { maxSize: 16 * 1024 })
- .toBytes()
- ),
- verifiedVaas[0],
- tx.object(SUI_CLOCK_OBJECT_ID),
- ],
- });
- } else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
- const verifiedVaas = await this.verifyVaas(updates, tx);
- [priceUpdatesHotPotato] = tx.moveCall({
- target: `${packageId}::pyth::create_price_infos_hot_potato`,
- arguments: [
- tx.object(this.pythStateId),
- tx.makeMoveVec({
- type: `${wormholePackageId}::vaa::VAA`,
- objects: verifiedVaas,
- }),
- tx.object(SUI_CLOCK_OBJECT_ID),
- ],
- });
- } else {
- throw new Error("Can't mix accumulator and non-accumulator messages");
- }
- const priceInfoObjects: ObjectId[] = [];
- 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 coin = tx.splitCoins(tx.gas, [
- tx.pure(await this.getBaseUpdateFee()),
- ]);
- [priceUpdatesHotPotato] = tx.moveCall({
- target: `${packageId}::pyth::update_single_price_feed`,
- arguments: [
- tx.object(this.pythStateId),
- priceUpdatesHotPotato,
- tx.object(priceInfoObjectId),
- coin,
- tx.object(SUI_CLOCK_OBJECT_ID),
- ],
- });
- }
- tx.moveCall({
- target: `${packageId}::hot_potato_vector::destroy`,
- arguments: [priceUpdatesHotPotato],
- typeArguments: [`${packageId}::price_info::PriceInfo`],
- });
- return priceInfoObjects;
- }
- async createPriceFeed(tx: TransactionBlock, updates: Buffer[]) {
- const wormholePackageId = await this.getWormholePackageId();
- const packageId = await this.getPythPackageId();
- if (updates.every((update) => this.isAccumulatorMsg(update))) {
- 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(
- builder
- .ser("vector<u8>", Array.from(updates[0]), { maxSize: 16 * 1024 })
- .toBytes()
- ),
- verifiedVaas[0],
- tx.object(SUI_CLOCK_OBJECT_ID),
- ],
- });
- } else if (updates.every((vaa) => !this.isAccumulatorMsg(vaa))) {
- const verifiedVaas = await this.verifyVaas(updates, tx);
- tx.moveCall({
- target: `${packageId}::pyth::create_price_feeds`,
- arguments: [
- tx.object(this.pythStateId),
- tx.makeMoveVec({
- type: `${wormholePackageId}::vaa::VAA`,
- objects: verifiedVaas,
- }),
- tx.object(SUI_CLOCK_OBJECT_ID),
- ],
- });
- } else {
- throw new Error("Can't mix accumulator and non-accumulator messages");
- }
- }
- /**
- * 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: HexString): Promise<ObjectId | undefined> {
- const normalizedFeedId = feedId.replace("0x", "");
- if (!this.priceFeedObjectIdCache.has(normalizedFeedId)) {
- const tableId = await this.getPriceTableId();
- const result = await this.provider.getDynamicFieldObject({
- parentId: tableId,
- name: {
- type: `${await this.getPythPackageId()}::price_identifier::PriceIdentifier`,
- value: {
- bytes: Array.from(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,
- 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 getPriceTableId(): Promise<ObjectId> {
- if (this.priceTableId === undefined) {
- const result = await this.provider.getDynamicFieldObject({
- parentId: this.pythStateId,
- name: {
- type: "vector<u8>",
- value: "price_info",
- },
- });
- if (!result.data) {
- throw new Error(
- "Price Table not found, contract may not be initialized"
- );
- }
- this.priceTableId = result.data.objectId;
- }
- return this.priceTableId;
- }
- /**
- * Checks if a message is an accumulator message or not
- * @param msg - update message from price service
- */
- isAccumulatorMsg(msg: Buffer) {
- const ACCUMULATOR_MAGIC = "504e4155";
- return msg.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC;
- }
- /**
- * 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: Buffer): Buffer {
- if (!this.isAccumulatorMsg(accumulatorMessage)) {
- throw new Error("Not an accumulator message");
- }
- // 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);
- }
- }
|