@mysten/suins
Version:
219 lines (217 loc) • 8.02 kB
JavaScript
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