@pythnetwork/pyth-sui-js
Version:
Pyth Network Sui Utilities
279 lines (278 loc) • 11.8 kB
JavaScript
"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;