@mysten/suins
Version:
1 lines • 13.9 kB
Source Map (JSON)
{"version":3,"file":"pyth.mjs","names":["#priceFeedObjectIdCache","#fetchPriceFeedObjectId","#priceTableInfo","#fetchPriceTableInfo","#wormholePackageId","#fetchWormholePackageId","WormholeState","#pythState","#fetchPythState","PythState","#getPythState"],"sources":["../../src/pyth/pyth.ts"],"sourcesContent":["// Copyright (c) Mysten Labs, Inc.\n// SPDX-License-Identifier: Apache-2.0\n\nimport { bcs } from '@mysten/sui/bcs';\nimport type { ClientWithCoreApi } from '@mysten/sui/client';\nimport type { Transaction, TransactionObjectArgument } from '@mysten/sui/transactions';\nimport { coinWithBalance } from '@mysten/sui/transactions';\nimport { fromBase64, fromHex, parseStructTag } from '@mysten/sui/utils';\n\nimport type { HexString } from './PriceServiceConnection.js';\nimport { PriceServiceConnection } from './PriceServiceConnection.js';\nimport { extractVaaBytesFromAccumulatorMessage } from './pyth-helpers.js';\nimport { State as PythState } from '../contracts/pyth/state.js';\nimport { State as WormholeState } from '../contracts/wormhole/state.js';\n\nconst MAX_ARGUMENT_SIZE = 16 * 1024;\nexport type ObjectId = string;\nexport class SuiPriceServiceConnection extends PriceServiceConnection {\n\t/**\n\t * Fetch price feed update data.\n\t *\n\t * @param priceIds Array of hex-encoded price IDs.\n\t * @returns Array of buffers containing the price update data.\n\t */\n\tasync getPriceFeedsUpdateData(priceIds: HexString[]): Promise<Uint8Array[]> {\n\t\tconst latestVaas = await this.getLatestVaas(priceIds);\n\t\treturn latestVaas.map((vaa) => fromBase64(vaa));\n\t}\n}\n\ntype ParsedPythState = ReturnType<typeof PythState.parse>;\n\nexport class SuiPythClient {\n\t#pythState?: Promise<ParsedPythState>;\n\t#wormholePackageId?: Promise<ObjectId>;\n\t#priceFeedObjectIdCache: Map<HexString, Promise<ObjectId>> = new Map();\n\t#priceTableInfo?: Promise<{ id: ObjectId; fieldType: ObjectId }>;\n\tprovider: ClientWithCoreApi;\n\tpythStateId: ObjectId;\n\twormholeStateId: ObjectId;\n\n\tconstructor(provider: ClientWithCoreApi, pythStateId: ObjectId, wormholeStateId: ObjectId) {\n\t\tthis.provider = provider;\n\t\tthis.pythStateId = pythStateId;\n\t\tthis.wormholeStateId = wormholeStateId;\n\t}\n\t/**\n\t * Verifies the VAAs using the Wormhole contract.\n\t *\n\t * @param vaas Array of VAA buffers to verify.\n\t * @param tx Transaction block to add commands to.\n\t * @returns Array of verified VAAs.\n\t */\n\tasync verifyVaas(vaas: Uint8Array[], tx: Transaction) {\n\t\tconst wormholePackageId = await this.getWormholePackageId();\n\t\tconst verifiedVaas = [];\n\t\tfor (const vaa of vaas) {\n\t\t\tconst [verifiedVaa] = tx.moveCall({\n\t\t\t\ttarget: `${wormholePackageId}::vaa::parse_and_verify`,\n\t\t\t\targuments: [tx.object(this.wormholeStateId), tx.pure.vector('u8', vaa), tx.object.clock()],\n\t\t\t});\n\t\t\tverifiedVaas.push(verifiedVaa);\n\t\t}\n\t\treturn verifiedVaas;\n\t}\n\t/**\n\t * Adds the necessary commands for updating the Pyth price feeds to the transaction block.\n\t *\n\t * @param tx Transaction block to add commands to.\n\t * @param updates Array of price feed updates received from the price service.\n\t * @param feedIds Array of feed IDs to update (in hex format).\n\t * @param feeCoin Optional custom SUI coin to use for Pyth oracle fees. If not provided, uses gas coin.\n\t */\n\tasync updatePriceFeeds(\n\t\ttx: Transaction,\n\t\tupdates: Uint8Array[],\n\t\tfeedIds: HexString[],\n\t\tfeeCoin?: TransactionObjectArgument,\n\t): Promise<ObjectId[]> {\n\t\tconst packageId = await this.getPythPackageId();\n\t\tlet priceUpdatesHotPotato;\n\t\tif (updates.length > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t'SDK does not support sending multiple accumulator messages in a single transaction',\n\t\t\t);\n\t\t}\n\t\tconst vaa = extractVaaBytesFromAccumulatorMessage(updates[0]);\n\t\tconst verifiedVaas = await this.verifyVaas([vaa], tx);\n\t\t[priceUpdatesHotPotato] = tx.moveCall({\n\t\t\ttarget: `${packageId}::pyth::create_authenticated_price_infos_using_accumulator`,\n\t\t\targuments: [\n\t\t\t\ttx.object(this.pythStateId),\n\t\t\t\ttx.pure(\n\t\t\t\t\tbcs\n\t\t\t\t\t\t.vector(bcs.U8)\n\t\t\t\t\t\t.serialize(Array.from(updates[0]), {\n\t\t\t\t\t\t\tmaxSize: MAX_ARGUMENT_SIZE,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.toBytes(),\n\t\t\t\t),\n\t\t\t\tverifiedVaas[0],\n\t\t\t\ttx.object.clock(),\n\t\t\t],\n\t\t});\n\t\tconst priceInfoObjects: ObjectId[] = [];\n\t\tconst baseUpdateFee = await this.getBaseUpdateFee();\n\t\tfor (const feedId of feedIds) {\n\t\t\tconst priceInfoObjectId = await this.getPriceFeedObjectId(feedId);\n\t\t\tif (!priceInfoObjectId) {\n\t\t\t\tthrow new Error(`Price feed ${feedId} not found, please create it first`);\n\t\t\t}\n\t\t\tpriceInfoObjects.push(priceInfoObjectId);\n\n\t\t\tconst feePayment = feeCoin\n\t\t\t\t? tx.splitCoins(feeCoin, [tx.pure.u64(baseUpdateFee)])[0]\n\t\t\t\t: coinWithBalance({ balance: baseUpdateFee });\n\n\t\t\t[priceUpdatesHotPotato] = tx.moveCall({\n\t\t\t\ttarget: `${packageId}::pyth::update_single_price_feed`,\n\t\t\t\targuments: [\n\t\t\t\t\ttx.object(this.pythStateId),\n\t\t\t\t\tpriceUpdatesHotPotato,\n\t\t\t\t\ttx.object(priceInfoObjectId),\n\t\t\t\t\tfeePayment,\n\t\t\t\t\ttx.object.clock(),\n\t\t\t\t],\n\t\t\t});\n\t\t}\n\t\ttx.moveCall({\n\t\t\ttarget: `${packageId}::hot_potato_vector::destroy`,\n\t\t\targuments: [priceUpdatesHotPotato],\n\t\t\ttypeArguments: [`${packageId}::price_info::PriceInfo`],\n\t\t});\n\t\treturn priceInfoObjects;\n\t}\n\t/**\n\t * Get the price feed object ID for a given feed ID, caching the promise.\n\t * @param feedId\n\t */\n\tgetPriceFeedObjectId(feedId: HexString): Promise<ObjectId | undefined> {\n\t\tif (!this.#priceFeedObjectIdCache.has(feedId)) {\n\t\t\tthis.#priceFeedObjectIdCache.set(\n\t\t\t\tfeedId,\n\t\t\t\tthis.#fetchPriceFeedObjectId(feedId).catch((err) => {\n\t\t\t\t\t// Remove failed promises from the cache to allow retries\n\t\t\t\t\tthis.#priceFeedObjectIdCache.delete(feedId);\n\t\t\t\t\tthrow err;\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\treturn this.#priceFeedObjectIdCache.get(feedId)!;\n\t}\n\n\t/**\n\t * Fetches the price feed object ID for a given feed ID (no caching).\n\t * Throws an error if the object is not found.\n\t */\n\tasync #fetchPriceFeedObjectId(feedId: HexString): Promise<ObjectId> {\n\t\tconst { id: tableId, fieldType } = await this.getPriceTableInfo();\n\n\t\tconst result = await this.provider.core.getDynamicField({\n\t\t\tparentId: tableId,\n\t\t\tname: {\n\t\t\t\ttype: `${fieldType}::price_identifier::PriceIdentifier`,\n\t\t\t\tbcs: bcs.byteVector().serialize(fromHex(feedId)).toBytes(),\n\t\t\t},\n\t\t});\n\n\t\tif (!result.dynamicField) {\n\t\t\tthrow new Error(`Price feed object ID for feed ID ${feedId} not found.`);\n\t\t}\n\n\t\treturn bcs.Address.parse(result.dynamicField.value.bcs);\n\t}\n\n\t/**\n\t * Fetches the price table object ID for the current state ID, caching the promise.\n\t * @returns Price table object ID and field type\n\t */\n\tgetPriceTableInfo(): Promise<{ id: ObjectId; fieldType: ObjectId }> {\n\t\tif (!this.#priceTableInfo) {\n\t\t\tconst promise = this.#fetchPriceTableInfo().catch((err) => {\n\t\t\t\t// Clear the cached promise on error\n\t\t\t\tthis.#priceTableInfo = undefined;\n\t\t\t\tthrow err;\n\t\t\t});\n\n\t\t\tthis.#priceTableInfo = promise;\n\t\t}\n\n\t\treturn this.#priceTableInfo;\n\t}\n\n\t/**\n\t * Fetches the price table object ID and field type (no caching).\n\t * @returns Price table object ID and field type\n\t */\n\tasync #fetchPriceTableInfo(): Promise<{ id: ObjectId; fieldType: ObjectId }> {\n\t\tconst result = await this.provider.core.getDynamicObjectField({\n\t\t\tparentId: this.pythStateId,\n\t\t\tname: {\n\t\t\t\ttype: 'vector<u8>',\n\t\t\t\tbcs: bcs.string().serialize('price_info').toBytes(),\n\t\t\t},\n\t\t});\n\n\t\tif (!result.object) {\n\t\t\tthrow new Error('Price Table not found, contract may not be initialized');\n\t\t}\n\n\t\tconst tableType = parseStructTag(result.object.type);\n\t\tconst priceIdentifier = tableType.typeParams[0];\n\t\tif (\n\t\t\ttypeof priceIdentifier === 'object' &&\n\t\t\tpriceIdentifier !== null &&\n\t\t\tpriceIdentifier.name === 'PriceIdentifier' &&\n\t\t\t'address' in priceIdentifier\n\t\t) {\n\t\t\treturn { id: result.object.objectId, fieldType: priceIdentifier.address };\n\t\t} else {\n\t\t\tthrow new Error('fieldType not found');\n\t\t}\n\t}\n\t/**\n\t * Fetches the package ID for the Wormhole contract, with caching.\n\t */\n\tgetWormholePackageId(): Promise<ObjectId> {\n\t\tif (!this.#wormholePackageId) {\n\t\t\tthis.#wormholePackageId = this.#fetchWormholePackageId();\n\t\t}\n\t\treturn this.#wormholePackageId;\n\t}\n\n\t/**\n\t * Fetches the package ID for the Wormhole contract (no caching).\n\t */\n\tasync #fetchWormholePackageId(): Promise<ObjectId> {\n\t\tconst result = await this.provider.core.getObject({\n\t\t\tobjectId: this.wormholeStateId,\n\t\t\tinclude: { content: true },\n\t\t});\n\n\t\tif (!result.object?.content) {\n\t\t\tthrow new Error('Unable to fetch Wormhole state object');\n\t\t}\n\n\t\tconst state = WormholeState.parse(result.object.content);\n\t\treturn state.upgrade_cap.package;\n\t}\n\n\t/**\n\t * Fetches and caches the parsed Pyth state object.\n\t * This is shared between getPythPackageId and getBaseUpdateFee to avoid redundant fetches.\n\t */\n\t#getPythState(): Promise<ParsedPythState> {\n\t\tif (!this.#pythState) {\n\t\t\tthis.#pythState = this.#fetchPythState();\n\t\t}\n\t\treturn this.#pythState;\n\t}\n\n\t/**\n\t * Fetches the Pyth state object (no caching).\n\t */\n\tasync #fetchPythState(): Promise<ParsedPythState> {\n\t\tconst result = await this.provider.core.getObject({\n\t\t\tobjectId: this.pythStateId,\n\t\t\tinclude: { content: true },\n\t\t});\n\n\t\tif (!result.object?.content) {\n\t\t\tthrow new Error('Unable to fetch Pyth state object');\n\t\t}\n\n\t\treturn PythState.parse(result.object.content);\n\t}\n\n\t/**\n\t * Fetches the package ID for the Pyth contract, with caching.\n\t * Uses the shared Pyth state cache.\n\t */\n\tasync getPythPackageId(): Promise<ObjectId> {\n\t\tconst state = await this.#getPythState();\n\t\treturn state.upgrade_cap.package;\n\t}\n\n\t/**\n\t * Returns the cached base update fee, fetching it if necessary.\n\t * Uses the shared Pyth state cache.\n\t */\n\tasync getBaseUpdateFee(): Promise<number> {\n\t\tconst state = await this.#getPythState();\n\t\treturn Number(state.base_update_fee);\n\t}\n}\n"],"mappings":";;;;;;;;;AAeA,MAAM,oBAAoB,KAAK;AAE/B,IAAa,4BAAb,cAA+C,uBAAuB;;;;;;;CAOrE,MAAM,wBAAwB,UAA8C;AAE3E,UADmB,MAAM,KAAK,cAAc,SAAS,EACnC,KAAK,QAAQ,WAAW,IAAI,CAAC;;;AAMjD,IAAa,gBAAb,MAA2B;CAC1B;CACA;CACA,0CAA6D,IAAI,KAAK;CACtE;CAKA,YAAY,UAA6B,aAAuB,iBAA2B;AAC1F,OAAK,WAAW;AAChB,OAAK,cAAc;AACnB,OAAK,kBAAkB;;;;;;;;;CASxB,MAAM,WAAW,MAAoB,IAAiB;EACrD,MAAM,oBAAoB,MAAM,KAAK,sBAAsB;EAC3D,MAAM,eAAe,EAAE;AACvB,OAAK,MAAM,OAAO,MAAM;GACvB,MAAM,CAAC,eAAe,GAAG,SAAS;IACjC,QAAQ,GAAG,kBAAkB;IAC7B,WAAW;KAAC,GAAG,OAAO,KAAK,gBAAgB;KAAE,GAAG,KAAK,OAAO,MAAM,IAAI;KAAE,GAAG,OAAO,OAAO;KAAC;IAC1F,CAAC;AACF,gBAAa,KAAK,YAAY;;AAE/B,SAAO;;;;;;;;;;CAUR,MAAM,iBACL,IACA,SACA,SACA,SACsB;EACtB,MAAM,YAAY,MAAM,KAAK,kBAAkB;EAC/C,IAAI;AACJ,MAAI,QAAQ,SAAS,EACpB,OAAM,IAAI,MACT,qFACA;EAEF,MAAM,MAAM,sCAAsC,QAAQ,GAAG;EAC7D,MAAM,eAAe,MAAM,KAAK,WAAW,CAAC,IAAI,EAAE,GAAG;AACrD,GAAC,yBAAyB,GAAG,SAAS;GACrC,QAAQ,GAAG,UAAU;GACrB,WAAW;IACV,GAAG,OAAO,KAAK,YAAY;IAC3B,GAAG,KACF,IACE,OAAO,IAAI,GAAG,CACd,UAAU,MAAM,KAAK,QAAQ,GAAG,EAAE,EAClC,SAAS,mBACT,CAAC,CACD,SAAS,CACX;IACD,aAAa;IACb,GAAG,OAAO,OAAO;IACjB;GACD,CAAC;EACF,MAAM,mBAA+B,EAAE;EACvC,MAAM,gBAAgB,MAAM,KAAK,kBAAkB;AACnD,OAAK,MAAM,UAAU,SAAS;GAC7B,MAAM,oBAAoB,MAAM,KAAK,qBAAqB,OAAO;AACjE,OAAI,CAAC,kBACJ,OAAM,IAAI,MAAM,cAAc,OAAO,oCAAoC;AAE1E,oBAAiB,KAAK,kBAAkB;GAExC,MAAM,aAAa,UAChB,GAAG,WAAW,SAAS,CAAC,GAAG,KAAK,IAAI,cAAc,CAAC,CAAC,CAAC,KACrD,gBAAgB,EAAE,SAAS,eAAe,CAAC;AAE9C,IAAC,yBAAyB,GAAG,SAAS;IACrC,QAAQ,GAAG,UAAU;IACrB,WAAW;KACV,GAAG,OAAO,KAAK,YAAY;KAC3B;KACA,GAAG,OAAO,kBAAkB;KAC5B;KACA,GAAG,OAAO,OAAO;KACjB;IACD,CAAC;;AAEH,KAAG,SAAS;GACX,QAAQ,GAAG,UAAU;GACrB,WAAW,CAAC,sBAAsB;GAClC,eAAe,CAAC,GAAG,UAAU,yBAAyB;GACtD,CAAC;AACF,SAAO;;;;;;CAMR,qBAAqB,QAAkD;AACtE,MAAI,CAAC,MAAKA,uBAAwB,IAAI,OAAO,CAC5C,OAAKA,uBAAwB,IAC5B,QACA,MAAKC,uBAAwB,OAAO,CAAC,OAAO,QAAQ;AAEnD,SAAKD,uBAAwB,OAAO,OAAO;AAC3C,SAAM;IACL,CACF;AAGF,SAAO,MAAKA,uBAAwB,IAAI,OAAO;;;;;;CAOhD,OAAMC,uBAAwB,QAAsC;EACnE,MAAM,EAAE,IAAI,SAAS,cAAc,MAAM,KAAK,mBAAmB;EAEjE,MAAM,SAAS,MAAM,KAAK,SAAS,KAAK,gBAAgB;GACvD,UAAU;GACV,MAAM;IACL,MAAM,GAAG,UAAU;IACnB,KAAK,IAAI,YAAY,CAAC,UAAU,QAAQ,OAAO,CAAC,CAAC,SAAS;IAC1D;GACD,CAAC;AAEF,MAAI,CAAC,OAAO,aACX,OAAM,IAAI,MAAM,oCAAoC,OAAO,aAAa;AAGzE,SAAO,IAAI,QAAQ,MAAM,OAAO,aAAa,MAAM,IAAI;;;;;;CAOxD,oBAAoE;AACnE,MAAI,CAAC,MAAKC,eAOT,OAAKA,iBANW,MAAKC,qBAAsB,CAAC,OAAO,QAAQ;AAE1D,SAAKD,iBAAkB;AACvB,SAAM;IACL;AAKH,SAAO,MAAKA;;;;;;CAOb,OAAMC,sBAAuE;EAC5E,MAAM,SAAS,MAAM,KAAK,SAAS,KAAK,sBAAsB;GAC7D,UAAU,KAAK;GACf,MAAM;IACL,MAAM;IACN,KAAK,IAAI,QAAQ,CAAC,UAAU,aAAa,CAAC,SAAS;IACnD;GACD,CAAC;AAEF,MAAI,CAAC,OAAO,OACX,OAAM,IAAI,MAAM,yDAAyD;EAI1E,MAAM,kBADY,eAAe,OAAO,OAAO,KAAK,CAClB,WAAW;AAC7C,MACC,OAAO,oBAAoB,YAC3B,oBAAoB,QACpB,gBAAgB,SAAS,qBACzB,aAAa,gBAEb,QAAO;GAAE,IAAI,OAAO,OAAO;GAAU,WAAW,gBAAgB;GAAS;MAEzE,OAAM,IAAI,MAAM,sBAAsB;;;;;CAMxC,uBAA0C;AACzC,MAAI,CAAC,MAAKC,kBACT,OAAKA,oBAAqB,MAAKC,wBAAyB;AAEzD,SAAO,MAAKD;;;;;CAMb,OAAMC,yBAA6C;;EAClD,MAAM,SAAS,MAAM,KAAK,SAAS,KAAK,UAAU;GACjD,UAAU,KAAK;GACf,SAAS,EAAE,SAAS,MAAM;GAC1B,CAAC;AAEF,MAAI,oBAAC,OAAO,wEAAQ,SACnB,OAAM,IAAI,MAAM,wCAAwC;AAIzD,SADcC,QAAc,MAAM,OAAO,OAAO,QAAQ,CAC3C,YAAY;;;;;;CAO1B,gBAA0C;AACzC,MAAI,CAAC,MAAKC,UACT,OAAKA,YAAa,MAAKC,gBAAiB;AAEzC,SAAO,MAAKD;;;;;CAMb,OAAMC,iBAA4C;;EACjD,MAAM,SAAS,MAAM,KAAK,SAAS,KAAK,UAAU;GACjD,UAAU,KAAK;GACf,SAAS,EAAE,SAAS,MAAM;GAC1B,CAAC;AAEF,MAAI,qBAAC,OAAO,0EAAQ,SACnB,OAAM,IAAI,MAAM,oCAAoC;AAGrD,SAAOC,MAAU,MAAM,OAAO,OAAO,QAAQ;;;;;;CAO9C,MAAM,mBAAsC;AAE3C,UADc,MAAM,MAAKC,cAAe,EAC3B,YAAY;;;;;;CAO1B,MAAM,mBAAoC;EACzC,MAAM,QAAQ,MAAM,MAAKA,cAAe;AACxC,SAAO,OAAO,MAAM,gBAAgB"}