pyth-crosschain/target_chains/sui/sdk/js/src/client.ts

302 lines
9.8 KiB
TypeScript

import { SuiClient } from "@mysten/sui.js/client";
import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui.js/utils";
import { TransactionBlock } from "@mysten/sui.js/transactions";
import { bcs } from "@mysten/sui.js/bcs";
import { HexString } from "@pythnetwork/price-service-client";
import { Buffer } from "buffer";
const MAX_ARGUMENT_SIZE = 16 * 1024;
export type ObjectId = string;
export class SuiPythClient {
private pythPackageId: ObjectId | undefined;
private wormholePackageId: ObjectId | undefined;
private priceTableInfo: { id: ObjectId; fieldType: ObjectId } | undefined;
private priceFeedObjectIdCache: Map<HexString, ObjectId> = new Map();
private baseUpdateFee: number | undefined;
constructor(
public provider: SuiClient,
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");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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;
}
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: 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(
bcs
.ser("vector<u8>", Array.from(vaa), {
maxSize: MAX_ARGUMENT_SIZE,
})
.toBytes()
),
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 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 = 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(
bcs
.ser("vector<u8>", Array.from(updates[0]), {
maxSize: MAX_ARGUMENT_SIZE,
})
.toBytes()
),
verifiedVaas[0],
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
const priceInfoObjects: ObjectId[] = [];
const baseUpdateFee = await this.getBaseUpdateFee();
const coins = tx.splitCoins(
tx.gas,
feedIds.map(() => tx.pure(baseUpdateFee))
);
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(SUI_CLOCK_OBJECT_ID),
],
});
coinId++;
}
tx.moveCall({
target: `${packageId}::hot_potato_vector::destroy`,
arguments: [priceUpdatesHotPotato],
typeArguments: [`${packageId}::price_info::PriceInfo`],
});
return priceInfoObjects;
}
async createPriceFeed(tx: TransactionBlock, updates: Buffer[]) {
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
.ser("vector<u8>", Array.from(updates[0]), {
maxSize: MAX_ARGUMENT_SIZE,
})
.toBytes()
),
verifiedVaas[0],
tx.object(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: HexString): Promise<ObjectId | undefined> {
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.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(): Promise<{ id: ObjectId; fieldType: ObjectId }> {
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: Buffer): Buffer {
// 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);
}
}