@pythnetwork/pyth-sui-js
Version:
Pyth Network Sui Utilities
285 lines (284 loc) • 12.3 kB
JavaScript
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-console */ "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "SuiPythClient", {
enumerable: true,
get: function() {
return SuiPythClient;
}
});
const _nodebuffer = require("node:buffer");
const _bcs = require("@mysten/sui/bcs");
const _utils = require("@mysten/sui/utils");
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?.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 - the object id
* @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
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
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.bcs.vector(_bcs.bcs.U8).serialize([
...vaa
], {
maxSize: MAX_ARGUMENT_SIZE
}).toBytes()),
tx.object(_utils.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.bcs.vector(_bcs.bcs.U8).serialize([
...updates[0]
], {
maxSize: MAX_ARGUMENT_SIZE
}).toBytes()),
verifiedVaas[0],
tx.object(_utils.SUI_CLOCK_OBJECT_ID)
]
});
return priceUpdatesHotPotato;
}
async executePriceFeedUpdates(tx, packageId, feedIds, // eslint-disable-next-line @typescript-eslint/no-explicit-any
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.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.bcs.vector(_bcs.bcs.U8).serialize([
...updates[0]
], {
maxSize: MAX_ARGUMENT_SIZE
}).toBytes()),
verifiedVaas[0],
tx.object(_utils.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 - the feed id
*/ 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: [
..._nodebuffer.Buffer.from(normalizedFeedId, "hex")
]
}
}
});
if (!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
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
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?.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);
}
}