From 23ff8b0e487923c4ca1db510e6d02a07fb87a5a7 Mon Sep 17 00:00:00 2001 From: atha Date: Wed, 22 Jul 2026 14:12:55 +0300 Subject: [PATCH 01/12] feat(suins): support Pyth Hermes v2 and keyed endpoints [SUIP-1111] Add an optional `pyth` config (`endpoint`, `accessToken`) to SuinsClient and forward it to the Pyth price-service connection, sending the token as an `Authorization: Bearer` header for keyed endpoints. Move the price-update fetch from the deprecated Hermes v1 (`/api/latest_vaas`) to v2 (`/v2/updates/price/latest`). Read the Pyth and Wormhole on-chain state via layout-agnostic JSON fields instead of BCS struct decoding, so a single build works against both the current and Pro-compatible Pyth package layouts. Verified end-to-end on mainnet against the keyed Pro endpoint + Pro state objects, and against the current public endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/suins-pyth-keyed-endpoint.md | 5 ++ .../suins/src/pyth/PriceServiceConnection.ts | 10 ++-- packages/suins/src/pyth/pyth.ts | 48 ++++++++++++------- packages/suins/src/suins-client.ts | 12 +++-- packages/suins/src/types.ts | 12 +++++ .../test/price-service-connection.test.ts | 47 ++++++++++++++++++ 6 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 .changeset/suins-pyth-keyed-endpoint.md create mode 100644 packages/suins/test/price-service-connection.test.ts diff --git a/.changeset/suins-pyth-keyed-endpoint.md b/.changeset/suins-pyth-keyed-endpoint.md new file mode 100644 index 000000000..ca8465cdc --- /dev/null +++ b/.changeset/suins-pyth-keyed-endpoint.md @@ -0,0 +1,5 @@ +--- +'@mysten/suins': minor +--- + +`SuinsClient` now accepts an optional `pyth` config (`endpoint`, `accessToken`) to point the Pyth Hermes price service at a custom or keyed endpoint. Price update data now uses the Hermes v2 API (`/v2/updates/price/latest`); the deprecated v1 `/api/latest_vaas` endpoint is no longer used. The Pyth and Wormhole on-chain state objects are now read via layout-agnostic JSON fields instead of BCS struct decoding, keeping the client compatible across both the current and Pro-compatible Pyth package layouts. diff --git a/packages/suins/src/pyth/PriceServiceConnection.ts b/packages/suins/src/pyth/PriceServiceConnection.ts index b9327dd64..45dc0b64d 100644 --- a/packages/suins/src/pyth/PriceServiceConnection.ts +++ b/packages/suins/src/pyth/PriceServiceConnection.ts @@ -12,6 +12,7 @@ export type PriceFeedRequestConfig = { export type PriceServiceConnectionConfig = { timeout?: number; httpRetries?: number; + accessToken?: string; }; export class PriceServiceConnection { private httpClient: AxiosInstance; @@ -25,6 +26,7 @@ export class PriceServiceConnection { this.httpClient = axios.create({ baseURL: endpoint, timeout: config?.timeout || 5000, + headers: config?.accessToken ? { Authorization: `Bearer ${config.accessToken}` } : undefined, }); axiosRetry(this.httpClient, { retries: config?.httpRetries || 3, @@ -38,11 +40,13 @@ export class PriceServiceConnection { * @returns Array of base64 encoded VAAs. */ async getLatestVaas(priceIds: HexString[]): Promise { - const response = await this.httpClient.get('/api/latest_vaas', { + const response = await this.httpClient.get('/v2/updates/price/latest', { params: { - ids: priceIds, + 'ids[]': priceIds, + encoding: 'base64', + parsed: false, }, }); - return response.data; + return response.data.binary.data; } } diff --git a/packages/suins/src/pyth/pyth.ts b/packages/suins/src/pyth/pyth.ts index 8a0a2c2c9..f86981448 100644 --- a/packages/suins/src/pyth/pyth.ts +++ b/packages/suins/src/pyth/pyth.ts @@ -10,8 +10,6 @@ import { fromBase64, fromHex, parseStructTag } from '@mysten/sui/utils'; import type { HexString } from './PriceServiceConnection.js'; import { PriceServiceConnection } from './PriceServiceConnection.js'; import { extractVaaBytesFromAccumulatorMessage } from './pyth-helpers.js'; -import { State as PythState } from '../contracts/pyth/state.js'; -import { State as WormholeState } from '../contracts/wormhole/state.js'; const MAX_ARGUMENT_SIZE = 16 * 1024; export type ObjectId = string; @@ -28,10 +26,22 @@ export class SuiPriceServiceConnection extends PriceServiceConnection { } } -type ParsedPythState = ReturnType; +type PythStateFields = { packageId: ObjectId; baseUpdateFee: number }; + +/** + * The `.core` JSON view represents a nested Move struct as `{ type, fields }` over + * JSON-RPC but flattens it to a plain field map over gRPC. Return the inner field map + * for either shape so callers can read fields transport-agnostically. + */ +function getStructFields(value: unknown): Record { + if (value && typeof value === 'object' && 'fields' in value) { + return (value as { fields: Record }).fields; + } + return value as Record; +} export class SuiPythClient { - #pythState?: Promise; + #pythState?: Promise; #wormholePackageId?: Promise; #priceFeedObjectIdCache: Map> = new Map(); #priceTableInfo?: Promise<{ id: ObjectId; fieldType: ObjectId }>; @@ -238,22 +248,21 @@ export class SuiPythClient { async #fetchWormholePackageId(): Promise { const result = await this.provider.core.getObject({ objectId: this.wormholeStateId, - include: { content: true }, + include: { json: true }, }); - if (!result.object?.content) { + if (!result.object?.json) { throw new Error('Unable to fetch Wormhole state object'); } - const state = WormholeState.parse(result.object.content); - return state.upgrade_cap.package; + return getStructFields(result.object.json.upgrade_cap).package as ObjectId; } /** * Fetches and caches the parsed Pyth state object. * This is shared between getPythPackageId and getBaseUpdateFee to avoid redundant fetches. */ - #getPythState(): Promise { + #getPythState(): Promise { if (!this.#pythState) { this.#pythState = this.#fetchPythState(); } @@ -261,19 +270,24 @@ export class SuiPythClient { } /** - * Fetches the Pyth state object (no caching). + * Fetches the Pyth state object (no caching). Reads named JSON fields instead of + * decoding the struct via BCS, so it stays compatible across Pyth package layouts. */ - async #fetchPythState(): Promise { + async #fetchPythState(): Promise { const result = await this.provider.core.getObject({ objectId: this.pythStateId, - include: { content: true }, + include: { json: true }, }); - if (!result.object?.content) { + if (!result.object?.json) { throw new Error('Unable to fetch Pyth state object'); } - return PythState.parse(result.object.content); + const state = result.object.json; + return { + packageId: getStructFields(state.upgrade_cap).package as ObjectId, + baseUpdateFee: Number(state.base_update_fee), + }; } /** @@ -281,8 +295,7 @@ export class SuiPythClient { * Uses the shared Pyth state cache. */ async getPythPackageId(): Promise { - const state = await this.#getPythState(); - return state.upgrade_cap.package; + return (await this.#getPythState()).packageId; } /** @@ -290,7 +303,6 @@ export class SuiPythClient { * Uses the shared Pyth state cache. */ async getBaseUpdateFee(): Promise { - const state = await this.#getPythState(); - return Number(state.base_update_fee); + return (await this.#getPythState()).baseUpdateFee; } } diff --git a/packages/suins/src/suins-client.ts b/packages/suins/src/suins-client.ts index f70ea046a..070e5bdaa 100644 --- a/packages/suins/src/suins-client.ts +++ b/packages/suins/src/suins-client.ts @@ -11,6 +11,7 @@ import type { CoinTypeDiscount, NameRecord, PackageInfo, + PythConnectionConfig, SuinsClientConfig, SuinsPriceList, } from './types.js'; @@ -62,10 +63,12 @@ export class SuinsClient { client: ClientWithCoreApi; network: SuiClientTypes.Network; config: PackageInfo; + pyth?: PythConnectionConfig; constructor(config: SuinsClientConfig) { this.client = config.client; this.network = config.network || 'mainnet'; + this.pyth = config.pyth; if (config.packageInfo) { this.config = config.packageInfo; @@ -286,10 +289,13 @@ export class SuinsClient { async getPriceInfoObject(tx: Transaction, feed: string, feeCoin?: TransactionObjectArgument) { const endpoint = - this.network === 'testnet' + this.pyth?.endpoint ?? + (this.network === 'testnet' ? 'https://hermes-beta.pyth.network' - : 'https://hermes.pyth.network'; - const connection = new SuiPriceServiceConnection(endpoint); + : 'https://hermes.pyth.network'); + const connection = new SuiPriceServiceConnection(endpoint, { + accessToken: this.pyth?.accessToken, + }); const priceIDs = [feed]; const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIDs); diff --git a/packages/suins/src/types.ts b/packages/suins/src/types.ts index ccbc131fa..c09f8c999 100644 --- a/packages/suins/src/types.ts +++ b/packages/suins/src/types.ts @@ -98,10 +98,22 @@ export type ReceiptParams = { priceInfoObjectId?: string | null; }; +/** + * Optional overrides for how the SDK reaches the Pyth Hermes price service. + * Needed once the public endpoint is retired and a keyed endpoint is required. + */ +export type PythConnectionConfig = { + /** Base URL of the Hermes endpoint. Defaults to the public host for the network. */ + endpoint?: string; + /** Bearer access token for keyed endpoints. Sent as `Authorization: Bearer `. */ + accessToken?: string; +}; + export type SuinsClientConfig = { client: ClientWithCoreApi; network?: SuiClientTypes.Network; packageInfo?: PackageInfo; + pyth?: PythConnectionConfig; }; export type SuinsPriceList = Map<[number, number], number>; diff --git a/packages/suins/test/price-service-connection.test.ts b/packages/suins/test/price-service-connection.test.ts new file mode 100644 index 000000000..fe6ee4d12 --- /dev/null +++ b/packages/suins/test/price-service-connection.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { PriceServiceConnection } from '../src/pyth/PriceServiceConnection.js'; + +const { mockGet, mockCreate } = vi.hoisted(() => { + const mockGet = vi.fn(); + const mockCreate = vi.fn(() => ({ get: mockGet })); + return { mockGet, mockCreate }; +}); + +vi.mock('axios', () => ({ default: { create: mockCreate } })); +vi.mock('axios-retry', () => ({ + default: Object.assign(() => {}, { exponentialDelay: () => {} }), +})); + +describe('PriceServiceConnection - Unit Tests', () => { + beforeEach(() => { + mockGet.mockReset(); + mockCreate.mockClear(); + }); + + describe('constructor()', () => { + it('builds a Bearer auth header from the access token', () => { + new PriceServiceConnection('https://host', { accessToken: 'secret' }); + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ headers: { Authorization: 'Bearer secret' } }), + ); + }); + }); + + describe('getLatestVaas()', () => { + it('fetches from the Hermes v2 endpoint', async () => { + mockGet.mockResolvedValue({ data: { binary: { data: ['v2-msg'] } } }); + const connection = new PriceServiceConnection('https://host'); + + const result = await connection.getLatestVaas(['0xfeed']); + + expect(mockGet).toHaveBeenCalledWith('/v2/updates/price/latest', { + params: { 'ids[]': ['0xfeed'], encoding: 'base64', parsed: false }, + }); + expect(result).toEqual(['v2-msg']); + }); + }); +}); From 142da3b292715499554e3c64eebfd46dd0df0e1c Mon Sep 17 00:00:00 2001 From: atha Date: Wed, 22 Jul 2026 15:26:28 +0300 Subject: [PATCH 02/12] refactor(suins): keep BCS state parsing for Pyth/Wormhole [SUIP-1111] The upgraded (Core->Pro) Pyth and Wormhole State structs have the same layout as the current ones, so the existing generated BCS bindings decode them unchanged. Revert the layout-agnostic JSON state reads added earlier; they are unnecessary. Verified end-to-end on mainnet: the current BCS bindings plus the upgraded Pyth/ Wormhole state ids verify keyed Pro price updates on-chain. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/suins-pyth-keyed-endpoint.md | 2 +- packages/suins/src/pyth/pyth.ts | 48 ++++++++++--------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/.changeset/suins-pyth-keyed-endpoint.md b/.changeset/suins-pyth-keyed-endpoint.md index ca8465cdc..32780c1c3 100644 --- a/.changeset/suins-pyth-keyed-endpoint.md +++ b/.changeset/suins-pyth-keyed-endpoint.md @@ -2,4 +2,4 @@ '@mysten/suins': minor --- -`SuinsClient` now accepts an optional `pyth` config (`endpoint`, `accessToken`) to point the Pyth Hermes price service at a custom or keyed endpoint. Price update data now uses the Hermes v2 API (`/v2/updates/price/latest`); the deprecated v1 `/api/latest_vaas` endpoint is no longer used. The Pyth and Wormhole on-chain state objects are now read via layout-agnostic JSON fields instead of BCS struct decoding, keeping the client compatible across both the current and Pro-compatible Pyth package layouts. +`SuinsClient` now accepts an optional `pyth` config (`endpoint`, `accessToken`) to point the Pyth Hermes price service at a custom or keyed endpoint. Price update data now uses the Hermes v2 API (`/v2/updates/price/latest`); the deprecated v1 `/api/latest_vaas` endpoint is no longer used. diff --git a/packages/suins/src/pyth/pyth.ts b/packages/suins/src/pyth/pyth.ts index f86981448..8a0a2c2c9 100644 --- a/packages/suins/src/pyth/pyth.ts +++ b/packages/suins/src/pyth/pyth.ts @@ -10,6 +10,8 @@ import { fromBase64, fromHex, parseStructTag } from '@mysten/sui/utils'; import type { HexString } from './PriceServiceConnection.js'; import { PriceServiceConnection } from './PriceServiceConnection.js'; import { extractVaaBytesFromAccumulatorMessage } from './pyth-helpers.js'; +import { State as PythState } from '../contracts/pyth/state.js'; +import { State as WormholeState } from '../contracts/wormhole/state.js'; const MAX_ARGUMENT_SIZE = 16 * 1024; export type ObjectId = string; @@ -26,22 +28,10 @@ export class SuiPriceServiceConnection extends PriceServiceConnection { } } -type PythStateFields = { packageId: ObjectId; baseUpdateFee: number }; - -/** - * The `.core` JSON view represents a nested Move struct as `{ type, fields }` over - * JSON-RPC but flattens it to a plain field map over gRPC. Return the inner field map - * for either shape so callers can read fields transport-agnostically. - */ -function getStructFields(value: unknown): Record { - if (value && typeof value === 'object' && 'fields' in value) { - return (value as { fields: Record }).fields; - } - return value as Record; -} +type ParsedPythState = ReturnType; export class SuiPythClient { - #pythState?: Promise; + #pythState?: Promise; #wormholePackageId?: Promise; #priceFeedObjectIdCache: Map> = new Map(); #priceTableInfo?: Promise<{ id: ObjectId; fieldType: ObjectId }>; @@ -248,21 +238,22 @@ export class SuiPythClient { async #fetchWormholePackageId(): Promise { const result = await this.provider.core.getObject({ objectId: this.wormholeStateId, - include: { json: true }, + include: { content: true }, }); - if (!result.object?.json) { + if (!result.object?.content) { throw new Error('Unable to fetch Wormhole state object'); } - return getStructFields(result.object.json.upgrade_cap).package as ObjectId; + const state = WormholeState.parse(result.object.content); + return state.upgrade_cap.package; } /** * Fetches and caches the parsed Pyth state object. * This is shared between getPythPackageId and getBaseUpdateFee to avoid redundant fetches. */ - #getPythState(): Promise { + #getPythState(): Promise { if (!this.#pythState) { this.#pythState = this.#fetchPythState(); } @@ -270,24 +261,19 @@ export class SuiPythClient { } /** - * Fetches the Pyth state object (no caching). Reads named JSON fields instead of - * decoding the struct via BCS, so it stays compatible across Pyth package layouts. + * Fetches the Pyth state object (no caching). */ - async #fetchPythState(): Promise { + async #fetchPythState(): Promise { const result = await this.provider.core.getObject({ objectId: this.pythStateId, - include: { json: true }, + include: { content: true }, }); - if (!result.object?.json) { + if (!result.object?.content) { throw new Error('Unable to fetch Pyth state object'); } - const state = result.object.json; - return { - packageId: getStructFields(state.upgrade_cap).package as ObjectId, - baseUpdateFee: Number(state.base_update_fee), - }; + return PythState.parse(result.object.content); } /** @@ -295,7 +281,8 @@ export class SuiPythClient { * Uses the shared Pyth state cache. */ async getPythPackageId(): Promise { - return (await this.#getPythState()).packageId; + const state = await this.#getPythState(); + return state.upgrade_cap.package; } /** @@ -303,6 +290,7 @@ export class SuiPythClient { * Uses the shared Pyth state cache. */ async getBaseUpdateFee(): Promise { - return (await this.#getPythState()).baseUpdateFee; + const state = await this.#getPythState(); + return Number(state.base_update_fee); } } From 97f7dc58a5f93e2692822b3c32215275e6555568 Mon Sep 17 00:00:00 2001 From: atha Date: Wed, 22 Jul 2026 16:09:57 +0300 Subject: [PATCH 03/12] feat(suins): target Pyth Pro keyed endpoint and upgraded state [SUIP-1111] Hard-code the single keyed Pyth Pro Hermes endpoint and drop the public host; the Pyth config collapses to a `pythAccessToken` on both `SuinsClient` and the `suins()` extension, sent as an `Authorization: Bearer` header. Point the Pyth/Wormhole state ids at the upgraded (Pro-compatible) deployments and converge the testnet price feeds to the global feed ids. Wire the live test to read `VITE_PYTH_ACCESS_TOKEN` from env (skipping when absent) and add a `.env.example`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/suins-pyth-keyed-endpoint.md | 4 ++-- packages/suins/.env.example | 3 +++ packages/suins/src/constants.ts | 12 ++++++------ packages/suins/src/suins-client.ts | 21 +++++++++++---------- packages/suins/src/types.ts | 14 ++------------ packages/suins/test/live.test.ts | 8 ++++++-- packages/suins/test/pre-built.ts | 2 +- 7 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 packages/suins/.env.example diff --git a/.changeset/suins-pyth-keyed-endpoint.md b/.changeset/suins-pyth-keyed-endpoint.md index 32780c1c3..9744c1a4a 100644 --- a/.changeset/suins-pyth-keyed-endpoint.md +++ b/.changeset/suins-pyth-keyed-endpoint.md @@ -1,5 +1,5 @@ --- -'@mysten/suins': minor +'@mysten/suins': major --- -`SuinsClient` now accepts an optional `pyth` config (`endpoint`, `accessToken`) to point the Pyth Hermes price service at a custom or keyed endpoint. Price update data now uses the Hermes v2 API (`/v2/updates/price/latest`); the deprecated v1 `/api/latest_vaas` endpoint is no longer used. +Pyth price fetching now uses the keyed Pyth Pro Hermes endpoint and requires a `pythAccessToken` (via `SuinsClient` or the `suins()` extension) for non-base-currency registrations and renewals. diff --git a/packages/suins/.env.example b/packages/suins/.env.example new file mode 100644 index 000000000..ffe8b787f --- /dev/null +++ b/packages/suins/.env.example @@ -0,0 +1,3 @@ +# Access token for the keyed Pyth Hermes endpoint (Pyth Pro). +# Required to run the live Pyth flow in test/live.test.ts; the test skips without it. +VITE_PYTH_ACCESS_TOKEN= diff --git a/packages/suins/src/constants.ts b/packages/suins/src/constants.ts index 925e408d0..4f74ca0ea 100644 --- a/packages/suins/src/constants.ts +++ b/packages/suins/src/constants.ts @@ -38,8 +38,8 @@ export const mainPackage: Config = { vault: '0x869f5100c0ecc0b35c7edad87ba3d488fd291bdba4a7aae84b70d188f440f393', }, pyth: { - pythStateId: '0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8', - wormholeStateId: '0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c', + pythStateId: '0x03719fae774ddab3cfcaa53bbc046f0cbe21410019b6280811bf3f9f4b05839d', + wormholeStateId: '0xdbca52b9fb4f712e25f61f974586d93ac541bcf8389564f0323bb07215168b5c', }, coins: { SUI: { @@ -80,20 +80,20 @@ export const mainPackage: Config = { vault: '0xa0b7a4dcbb85209c9096a4e0e85e43b716377c605743193abe915e9c9f3043e5', }, pyth: { - pythStateId: '0x243759059f4c3111179da5878c12f68d612c21a8d54d85edc86164bb18be1c7c', - wormholeStateId: '0x31358d198147da50db32eda2562951d53973a0c0ad5ed738e9b17d88b213d790', + pythStateId: '0x3c48fe392912de6c18087a2b3f5fdbfbfdb4598e180947feff1f12f8e9ea073e', + wormholeStateId: '0x750da8e6d16b6a363a39fe2eaa8295ac224a1e6fce4e47b58845e2e8746164f0', }, /// Testnet coins will be different here for testing purposes, we can publish our own coins: { SUI: { type: '0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI', - feed: '0x50c67b3fd225db8912a424dd4baed60ffdde625ed2feaaf283724f9608fea266', + feed: '0x23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744', }, /// this is a test token published as 0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTNS::TESTNS /// NS token is using the HFT feed since NS feed on testnet is not available NS: { type: '0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTNS::TESTNS', - feed: '0x99137a18354efa7fb6840889d059fdb04c46a6ce21be97ab60d9ad93e91ac758', + feed: '0xbb5ff26e47a3a6cc7ec2fce1db996c2a145300edc5acaabe43bf9ff7c5dd5d32', }, /// this is a test token published as 0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTUSDC::TESTUSDC USDC: { diff --git a/packages/suins/src/suins-client.ts b/packages/suins/src/suins-client.ts index 070e5bdaa..69be2245f 100644 --- a/packages/suins/src/suins-client.ts +++ b/packages/suins/src/suins-client.ts @@ -11,7 +11,6 @@ import type { CoinTypeDiscount, NameRecord, PackageInfo, - PythConnectionConfig, SuinsClientConfig, SuinsPriceList, } from './types.js'; @@ -22,9 +21,14 @@ import { NameRecord as NameRecordBcs } from './contracts/suins/name_record.js'; import { PricingConfig, RenewalConfig } from './contracts/suins/pricing_config.js'; import { PaymentsConfig } from './contracts/suins_payments/payments.js'; +/** Keyed Pyth Hermes endpoint. A single host serves all networks under Pyth Pro. */ +const HERMES_ENDPOINT = 'https://pyth.dourolabs.app/hermes'; + export type SuinsExtensionOptions = { name?: Name; packageInfo?: PackageInfo; + /** Access token for the keyed Pyth Hermes endpoint. Sent as `Authorization: Bearer `. */ + pythAccessToken?: string; }; /** @@ -46,6 +50,7 @@ export type SuinsExtensionOptions = { export function suins({ name = 'suins' as Name, packageInfo, + pythAccessToken, }: SuinsExtensionOptions = {}) { return { name, @@ -54,6 +59,7 @@ export function suins({ client, network: client.network, packageInfo, + pythAccessToken, }); }, }; @@ -63,12 +69,12 @@ export class SuinsClient { client: ClientWithCoreApi; network: SuiClientTypes.Network; config: PackageInfo; - pyth?: PythConnectionConfig; + pythAccessToken?: string; constructor(config: SuinsClientConfig) { this.client = config.client; this.network = config.network || 'mainnet'; - this.pyth = config.pyth; + this.pythAccessToken = config.pythAccessToken; if (config.packageInfo) { this.config = config.packageInfo; @@ -288,13 +294,8 @@ export class SuinsClient { } async getPriceInfoObject(tx: Transaction, feed: string, feeCoin?: TransactionObjectArgument) { - const endpoint = - this.pyth?.endpoint ?? - (this.network === 'testnet' - ? 'https://hermes-beta.pyth.network' - : 'https://hermes.pyth.network'); - const connection = new SuiPriceServiceConnection(endpoint, { - accessToken: this.pyth?.accessToken, + const connection = new SuiPriceServiceConnection(HERMES_ENDPOINT, { + accessToken: this.pythAccessToken, }); const priceIDs = [feed]; const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIDs); diff --git a/packages/suins/src/types.ts b/packages/suins/src/types.ts index c09f8c999..525a486f2 100644 --- a/packages/suins/src/types.ts +++ b/packages/suins/src/types.ts @@ -98,22 +98,12 @@ export type ReceiptParams = { priceInfoObjectId?: string | null; }; -/** - * Optional overrides for how the SDK reaches the Pyth Hermes price service. - * Needed once the public endpoint is retired and a keyed endpoint is required. - */ -export type PythConnectionConfig = { - /** Base URL of the Hermes endpoint. Defaults to the public host for the network. */ - endpoint?: string; - /** Bearer access token for keyed endpoints. Sent as `Authorization: Bearer `. */ - accessToken?: string; -}; - export type SuinsClientConfig = { client: ClientWithCoreApi; network?: SuiClientTypes.Network; packageInfo?: PackageInfo; - pyth?: PythConnectionConfig; + /** Access token for the keyed Pyth Hermes endpoint. Sent as `Authorization: Bearer `. */ + pythAccessToken?: string; }; export type SuinsPriceList = Map<[number, number], number>; diff --git a/packages/suins/test/live.test.ts b/packages/suins/test/live.test.ts index 4d738f9f6..5281189cc 100644 --- a/packages/suins/test/live.test.ts +++ b/packages/suins/test/live.test.ts @@ -5,8 +5,12 @@ import { describe, expect, it } from 'vitest'; import { e2eLiveNetworkDryRunFlow } from './pre-built.js'; +// The live flow hits the keyed Pyth Hermes endpoint, which needs an access token. +// Skip when it is not provided so the suite doesn't fail for contributors without one. +const hasPythKey = Boolean(process.env.VITE_PYTH_ACCESS_TOKEN); + describe('it should work on live networks', () => { - it('should work on mainnet', async () => { + it.skipIf(!hasPythKey)('should work on mainnet', async () => { const res = await e2eLiveNetworkDryRunFlow('mainnet'); if (res.FailedTransaction) { throw new Error(`Transaction failed: ${JSON.stringify(res.FailedTransaction?.status.error)}`); @@ -15,7 +19,7 @@ describe('it should work on live networks', () => { expect(res.Transaction.status.success).toEqual(true); }); - it('should work on testnet', async () => { + it.skipIf(!hasPythKey)('should work on testnet', async () => { const res = await e2eLiveNetworkDryRunFlow('testnet'); if (res.FailedTransaction) { throw new Error(`Transaction failed: ${JSON.stringify(res.FailedTransaction?.status.error)}`); diff --git a/packages/suins/test/pre-built.ts b/packages/suins/test/pre-built.ts index 7d05f89b6..a55d81806 100644 --- a/packages/suins/test/pre-built.ts +++ b/packages/suins/test/pre-built.ts @@ -10,7 +10,7 @@ import { ALLOWED_METADATA, SuinsTransaction, suins } from '../src/index.js'; export const e2eLiveNetworkDryRunFlow = async (network: 'mainnet' | 'testnet') => { const client = new SuiGrpcClient({ baseUrl: getJsonRpcFullnodeUrl(network), network }).$extend( - suins(), + suins({ pythAccessToken: process.env.VITE_PYTH_ACCESS_TOKEN }), ); const sender = normalizeSuiAddress('0x2'); From c30f4ede79f04dde0455a17d0766567a8cf0d8bc Mon Sep 17 00:00:00 2001 From: atha Date: Mon, 27 Jul 2026 17:57:41 +0300 Subject: [PATCH 04/12] feat(suins): switch payments flow to Pro _pro entrypoints on testnet - bump testnet payments/bbb package ids to the upgraded published-at - add payments.packageIdV1 to keep the original id for PaymentsConfig type identity, since an in-place upgrade moves only the call target - regenerate the payments binding with handle_payment_pro/calculate_price_pro - call the _pro entrypoints in the non-base register/renew path Testnet e2e reaches calculate_price_pro and aborts only on EPriceFeedIdMismatch, pending the on-chain feed-id admin update. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/suins/src/constants.ts | 6 +- .../src/contracts/suins_payments/payments.ts | 126 ++++++++++++++---- packages/suins/src/suins-client.ts | 2 +- packages/suins/src/suins-transaction.ts | 4 +- packages/suins/src/types.ts | 1 + 5 files changed, 108 insertions(+), 31 deletions(-) diff --git a/packages/suins/src/constants.ts b/packages/suins/src/constants.ts index 4f74ca0ea..0286c73fb 100644 --- a/packages/suins/src/constants.ts +++ b/packages/suins/src/constants.ts @@ -32,6 +32,7 @@ export const mainPackage: Config = { }, payments: { packageId: '0xdd0a4a34152a80d7841710e916a407b2a62961eee5b2188dcfdaa24194f66286', + packageIdV1: '0xdd0a4a34152a80d7841710e916a407b2a62961eee5b2188dcfdaa24194f66286', }, bbb: { packageId: '0x6268d072063a311f6f0a1db516d06d97c06a3fa6d10e797cad578937a47b3992', @@ -73,10 +74,11 @@ export const mainPackage: Config = { packageId: '0x63029aae8abbefae4f4ac6c5e3e0021159ea93a94ba648681fd64caf5b40677a', }, payments: { - packageId: '0xc391c200188dd1a363ff12dcffe07eaac5cf28ad1cd8dc0fcc18f2f8625f0da2', + packageId: '0x4f33a0e1e30530f2aa500a41b9e3d502f8af3ef2c20bd0a1e42374e329da7cb0', + packageIdV1: '0xc391c200188dd1a363ff12dcffe07eaac5cf28ad1cd8dc0fcc18f2f8625f0da2', }, bbb: { - packageId: '0xed9b18147ca81c8f3f60192c8d0630574e42387cd200a6e39b3e4e07df1ce6e6', + packageId: '0xab7f8da0f974ae38c205d9351787ae938da65c0a0e81d9788014f5f62a917aa1', vault: '0xa0b7a4dcbb85209c9096a4e0e85e43b716377c605743193abe915e9c9f3043e5', }, pyth: { diff --git a/packages/suins/src/contracts/suins_payments/payments.ts b/packages/suins/src/contracts/suins_payments/payments.ts index c59af85f7..afcb54e72 100644 --- a/packages/suins/src/contracts/suins_payments/payments.ts +++ b/packages/suins/src/contracts/suins_payments/payments.ts @@ -73,6 +73,57 @@ export function handleBasePayment(options: HandleBasePaymentOptions) { }); } export interface HandlePaymentArguments { + Suins: RawTransactionArgument; + BbbVault: RawTransactionArgument; + Intent: TransactionArgument; + Payment: RawTransactionArgument; + PriceInfoObject: RawTransactionArgument; + UserPriceGuard: RawTransactionArgument; +} +export interface HandlePaymentOptions { + package?: string; + arguments: + | HandlePaymentArguments + | [ + Suins: RawTransactionArgument, + BbbVault: RawTransactionArgument, + Intent: TransactionArgument, + Payment: RawTransactionArgument, + PriceInfoObject: RawTransactionArgument, + UserPriceGuard: RawTransactionArgument, + ]; + typeArguments: [string]; +} +/** + * Deprecated after the Pyth Core to Pro cutover: reads the Core feed, which stops + * updating. The signature is retained for upgrade compatibility, but the body is + * disabled. Use `handle_payment_pro` instead. Callers needing the Core feed can + * still target the pre-upgrade package version. + */ +export function handlePayment(options: HandlePaymentOptions) { + const packageAddress = options.package ?? '@suins/payments'; + const argumentsTypes = [null, null, null, null, '0x2::clock::Clock', null, 'u64'] satisfies ( + | string + | null + )[]; + const parameterNames = [ + 'Suins', + 'BbbVault', + 'Intent', + 'Payment', + 'PriceInfoObject', + 'UserPriceGuard', + ]; + return (tx: Transaction) => + tx.moveCall({ + package: packageAddress, + module: 'payments', + function: 'handle_payment', + arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + typeArguments: options.typeArguments, + }); +} +export interface HandlePaymentProArguments { suins: RawTransactionArgument; bbbVault: RawTransactionArgument; intent: TransactionArgument; @@ -80,10 +131,10 @@ export interface HandlePaymentArguments { priceInfoObject: RawTransactionArgument; userPriceGuard: RawTransactionArgument; } -export interface HandlePaymentOptions { +export interface HandlePaymentProOptions { package?: string; arguments: - | HandlePaymentArguments + | HandlePaymentProArguments | [ suins: RawTransactionArgument, bbbVault: RawTransactionArgument, @@ -95,18 +146,11 @@ export interface HandlePaymentOptions { typeArguments: [string]; } /** - * Handles a payment done for a non-base currency payment. E.g. SUI, NS. - * - * The payment amount is derived from the base currency price and the Pyth price - * feed. - * - * The `user_price_guard` is a value that the user expects to pay. If the payment - * amount is higher than this value, the payment will be rejected. This is to - * protect the user from paying more than they expected on their FEs. Ideally, this - * number should be calculated on the FE based on the price that is being displayed - * to the user (with a buffer determined by the FE). + * `handle_payment` variant that reads the Pro-compatible Pyth feed, for use after + * the Pyth Core to Pro cutover. Behaviour matches `handle_payment`; only the price + * source differs. */ -export function handlePayment(options: HandlePaymentOptions) { +export function handlePaymentPro(options: HandlePaymentProOptions) { const packageAddress = options.package ?? '@suins/payments'; const argumentsTypes = [null, null, null, null, '0x2::clock::Clock', null, 'u64'] satisfies ( | string @@ -124,20 +168,55 @@ export function handlePayment(options: HandlePaymentOptions) { tx.moveCall({ package: packageAddress, module: 'payments', - function: 'handle_payment', + function: 'handle_payment_pro', arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), typeArguments: options.typeArguments, }); } export interface CalculatePriceArguments { + Suins: RawTransactionArgument; + BaseAmount: RawTransactionArgument; + PriceInfoObject: RawTransactionArgument; +} +export interface CalculatePriceOptions { + package?: string; + arguments: + | CalculatePriceArguments + | [ + Suins: RawTransactionArgument, + BaseAmount: RawTransactionArgument, + PriceInfoObject: RawTransactionArgument, + ]; + typeArguments: [string]; +} +/** + * Deprecated after the Pyth Core to Pro cutover: reads the Core feed, which stops + * updating. The signature is retained for upgrade compatibility, but the body is + * disabled. Use `calculate_price_pro` instead. Callers needing the Core feed can + * still target the pre-upgrade package version. + */ +export function calculatePrice(options: CalculatePriceOptions) { + const packageAddress = options.package ?? '@suins/payments'; + const argumentsTypes = [null, 'u64', '0x2::clock::Clock', null] satisfies (string | null)[]; + const parameterNames = ['Suins', 'BaseAmount', 'PriceInfoObject']; + return (tx: Transaction) => + tx.moveCall({ + package: packageAddress, + module: 'payments', + function: 'calculate_price', + arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + typeArguments: options.typeArguments, + }); +} +export interface CalculatePriceProArguments { suins: RawTransactionArgument; baseAmount: RawTransactionArgument; priceInfoObject: RawTransactionArgument; } -export interface CalculatePriceOptions { +export interface CalculatePriceProOptions { package?: string; arguments: - | CalculatePriceArguments + | CalculatePriceProArguments | [ suins: RawTransactionArgument, baseAmount: RawTransactionArgument, @@ -146,16 +225,11 @@ export interface CalculatePriceOptions { typeArguments: [string]; } /** - * Calculates the amount that has to be paid in the target currency. - * - * Can be used to split the payment amount in a single PTB. - * - * 1. const intent = function_to_get_intent(); - * 2. const price = calculate_price(suins, intent, ...); - * 3. const coin = txb.splitCoins(baseCoin, [price]) - * 4. handle_payment(suins, intent, coin, ...); + * `calculate_price` variant that reads the Pro-compatible Pyth feed, for use after + * the Pyth Core to Pro cutover. Behaviour matches `calculate_price`; only the + * price source differs. */ -export function calculatePrice(options: CalculatePriceOptions) { +export function calculatePricePro(options: CalculatePriceProOptions) { const packageAddress = options.package ?? '@suins/payments'; const argumentsTypes = [null, 'u64', '0x2::clock::Clock', null] satisfies (string | null)[]; const parameterNames = ['suins', 'baseAmount', 'priceInfoObject']; @@ -163,7 +237,7 @@ export function calculatePrice(options: CalculatePriceOptions) { tx.moveCall({ package: packageAddress, module: 'payments', - function: 'calculate_price', + function: 'calculate_price_pro', arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), typeArguments: options.typeArguments, }); diff --git a/packages/suins/src/suins-client.ts b/packages/suins/src/suins-client.ts index 69be2245f..d3452d31c 100644 --- a/packages/suins/src/suins-client.ts +++ b/packages/suins/src/suins-client.ts @@ -177,7 +177,7 @@ export class SuinsClient { if (!this.config.suins) throw new Error('Suins object ID is not set'); if (!this.config.packageId) throw new Error('Price list config not found'); - const configType = `${this.config.packageIdV1}::suins::ConfigKey<${this.config.payments.packageId}::payments::PaymentsConfig>`; + const configType = `${this.config.packageIdV1}::suins::ConfigKey<${this.config.payments.packageIdV1}::payments::PaymentsConfig>`; const result = await this.client.core.getDynamicField({ parentId: this.config.suins, diff --git a/packages/suins/src/suins-transaction.ts b/packages/suins/src/suins-transaction.ts index a77bf7834..2f5869e72 100644 --- a/packages/suins/src/suins-transaction.ts +++ b/packages/suins/src/suins-transaction.ts @@ -132,7 +132,7 @@ export class SuinsTransaction { ): TransactionObjectArgument { const config = this.suinsClient.config; return this.transaction.add( - paymentsModule.calculatePrice({ + paymentsModule.calculatePricePro({ package: config.payments.packageId, arguments: { suins: config.suins, @@ -173,7 +173,7 @@ export class SuinsTransaction { ): TransactionObjectArgument { const config = this.suinsClient.config; return this.transaction.add( - paymentsModule.handlePayment({ + paymentsModule.handlePaymentPro({ package: config.payments.packageId, arguments: { suins: config.suins, diff --git a/packages/suins/src/types.ts b/packages/suins/src/types.ts index 525a486f2..497e41a85 100644 --- a/packages/suins/src/types.ts +++ b/packages/suins/src/types.ts @@ -33,6 +33,7 @@ export interface PackageInfo { }; payments: { packageId: string; + packageIdV1: string; }; bbb: { packageId: string; From d159c2e6ea760088b7ed967edb445c86ba0dfed9 Mon Sep 17 00:00:00 2001 From: atha Date: Mon, 27 Jul 2026 20:49:49 +0300 Subject: [PATCH 05/12] test(suins): drop mock-only PriceServiceConnection unit test The two cases only asserted the code calls axios the way it calls axios (change-detectors against mocks). The real Pyth Pro path is covered by the live e2e, so this added no behavioral coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/price-service-connection.test.ts | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 packages/suins/test/price-service-connection.test.ts diff --git a/packages/suins/test/price-service-connection.test.ts b/packages/suins/test/price-service-connection.test.ts deleted file mode 100644 index fe6ee4d12..000000000 --- a/packages/suins/test/price-service-connection.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { PriceServiceConnection } from '../src/pyth/PriceServiceConnection.js'; - -const { mockGet, mockCreate } = vi.hoisted(() => { - const mockGet = vi.fn(); - const mockCreate = vi.fn(() => ({ get: mockGet })); - return { mockGet, mockCreate }; -}); - -vi.mock('axios', () => ({ default: { create: mockCreate } })); -vi.mock('axios-retry', () => ({ - default: Object.assign(() => {}, { exponentialDelay: () => {} }), -})); - -describe('PriceServiceConnection - Unit Tests', () => { - beforeEach(() => { - mockGet.mockReset(); - mockCreate.mockClear(); - }); - - describe('constructor()', () => { - it('builds a Bearer auth header from the access token', () => { - new PriceServiceConnection('https://host', { accessToken: 'secret' }); - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ headers: { Authorization: 'Bearer secret' } }), - ); - }); - }); - - describe('getLatestVaas()', () => { - it('fetches from the Hermes v2 endpoint', async () => { - mockGet.mockResolvedValue({ data: { binary: { data: ['v2-msg'] } } }); - const connection = new PriceServiceConnection('https://host'); - - const result = await connection.getLatestVaas(['0xfeed']); - - expect(mockGet).toHaveBeenCalledWith('/v2/updates/price/latest', { - params: { 'ids[]': ['0xfeed'], encoding: 'base64', parsed: false }, - }); - expect(result).toEqual(['v2-msg']); - }); - }); -}); From 378a7b7ec91620b9dfbff39ffce0b7ced5099dce Mon Sep 17 00:00:00 2001 From: atha Date: Mon, 27 Jul 2026 20:52:18 +0300 Subject: [PATCH 06/12] refactor(suins): inline Pyth endpoint as a local var again Keep the endpoint where it was on main (a local in getPriceInfoObject) instead of a module constant, to minimize the diff. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/suins/src/suins-client.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/suins/src/suins-client.ts b/packages/suins/src/suins-client.ts index d3452d31c..c8b9dd6aa 100644 --- a/packages/suins/src/suins-client.ts +++ b/packages/suins/src/suins-client.ts @@ -21,9 +21,6 @@ import { NameRecord as NameRecordBcs } from './contracts/suins/name_record.js'; import { PricingConfig, RenewalConfig } from './contracts/suins/pricing_config.js'; import { PaymentsConfig } from './contracts/suins_payments/payments.js'; -/** Keyed Pyth Hermes endpoint. A single host serves all networks under Pyth Pro. */ -const HERMES_ENDPOINT = 'https://pyth.dourolabs.app/hermes'; - export type SuinsExtensionOptions = { name?: Name; packageInfo?: PackageInfo; @@ -294,7 +291,8 @@ export class SuinsClient { } async getPriceInfoObject(tx: Transaction, feed: string, feeCoin?: TransactionObjectArgument) { - const connection = new SuiPriceServiceConnection(HERMES_ENDPOINT, { + const endpoint = 'https://pyth.dourolabs.app/hermes'; + const connection = new SuiPriceServiceConnection(endpoint, { accessToken: this.pythAccessToken, }); const priceIDs = [feed]; From 55ac1efb5622913a3f1cc92c7127ce346ed6cad9 Mon Sep 17 00:00:00 2001 From: atha Date: Tue, 28 Jul 2026 11:36:03 +0300 Subject: [PATCH 07/12] feat(suins): throw early when pythAccessToken is missing getPriceInfoObject is the only consumer of the token; guard it so a missing token fails fast with a clear message instead of an opaque 401 from the keyed Pro Hermes endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/suins/src/suins-client.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/suins/src/suins-client.ts b/packages/suins/src/suins-client.ts index c8b9dd6aa..3de1a3ac8 100644 --- a/packages/suins/src/suins-client.ts +++ b/packages/suins/src/suins-client.ts @@ -291,6 +291,12 @@ export class SuinsClient { } async getPriceInfoObject(tx: Transaction, feed: string, feeCoin?: TransactionObjectArgument) { + if (!this.pythAccessToken) { + throw new Error( + 'A `pythAccessToken` is required to fetch Pyth price updates from the keyed Pro Hermes endpoint.', + ); + } + const endpoint = 'https://pyth.dourolabs.app/hermes'; const connection = new SuiPriceServiceConnection(endpoint, { accessToken: this.pythAccessToken, From d52164bce66e1e3a6b4b821ec11c781dfb193aed Mon Sep 17 00:00:00 2001 From: atha Date: Tue, 28 Jul 2026 11:44:31 +0300 Subject: [PATCH 08/12] refactor(suins): make pythAccessToken readonly It's a credential only read inside the class; readonly prevents reassignment. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/suins/src/suins-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/suins/src/suins-client.ts b/packages/suins/src/suins-client.ts index 3de1a3ac8..804d843db 100644 --- a/packages/suins/src/suins-client.ts +++ b/packages/suins/src/suins-client.ts @@ -66,7 +66,7 @@ export class SuinsClient { client: ClientWithCoreApi; network: SuiClientTypes.Network; config: PackageInfo; - pythAccessToken?: string; + readonly pythAccessToken?: string; constructor(config: SuinsClientConfig) { this.client = config.client; From ffede08116e6a37dfd537984334ab9eb4abc7f55 Mon Sep 17 00:00:00 2001 From: atha Date: Tue, 28 Jul 2026 11:58:55 +0300 Subject: [PATCH 09/12] fix(suins): guard the Hermes response shape in getLatestVaas Throw a legible error if `binary.data` is missing instead of a bare "Cannot read properties of undefined" TypeError when the shape drifts. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/suins/src/pyth/PriceServiceConnection.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/suins/src/pyth/PriceServiceConnection.ts b/packages/suins/src/pyth/PriceServiceConnection.ts index 45dc0b64d..08f1eddf6 100644 --- a/packages/suins/src/pyth/PriceServiceConnection.ts +++ b/packages/suins/src/pyth/PriceServiceConnection.ts @@ -47,6 +47,10 @@ export class PriceServiceConnection { parsed: false, }, }); - return response.data.binary.data; + const data = response.data?.binary?.data; + if (!Array.isArray(data)) { + throw new Error('Unexpected Hermes response: missing `binary.data`.'); + } + return data; } } From 106559ff9315675003d743fbd72a749b2ca89f60 Mon Sep 17 00:00:00 2001 From: atha Date: Tue, 28 Jul 2026 12:05:14 +0300 Subject: [PATCH 10/12] docs(suins): drop stale testnet NS feed comment Testnet NS now points at the global NS feed (same as mainnet), so the "using the HFT feed since NS feed on testnet is not available" note no longer applies. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/suins/src/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/suins/src/constants.ts b/packages/suins/src/constants.ts index 0286c73fb..6525cb3e9 100644 --- a/packages/suins/src/constants.ts +++ b/packages/suins/src/constants.ts @@ -92,7 +92,6 @@ export const mainPackage: Config = { feed: '0x23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744', }, /// this is a test token published as 0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTNS::TESTNS - /// NS token is using the HFT feed since NS feed on testnet is not available NS: { type: '0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTNS::TESTNS', feed: '0xbb5ff26e47a3a6cc7ec2fce1db996c2a145300edc5acaabe43bf9ff7c5dd5d32', From e9a3b386d2c701f504cd36301c71dcdbf31abb9c Mon Sep 17 00:00:00 2001 From: Nataly Date: Wed, 19 Aug 2026 00:24:09 +0300 Subject: [PATCH 11/12] [SUIP-1174] chore(suins): bump mainnet payments/bbb package ids --- packages/suins/src/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/suins/src/constants.ts b/packages/suins/src/constants.ts index 6525cb3e9..6edd39d65 100644 --- a/packages/suins/src/constants.ts +++ b/packages/suins/src/constants.ts @@ -31,11 +31,11 @@ export const mainPackage: Config = { packageId: '0xb162340524e0697461c307b9dc530c17e837b0f2c6d7f787da40d29d29681e5e', }, payments: { - packageId: '0xdd0a4a34152a80d7841710e916a407b2a62961eee5b2188dcfdaa24194f66286', + packageId: '0xdbbf23390d9fb0dc0cf05c701ee61b02ae648268bff8c7ed4fe3e3128ec90b99', packageIdV1: '0xdd0a4a34152a80d7841710e916a407b2a62961eee5b2188dcfdaa24194f66286', }, bbb: { - packageId: '0x6268d072063a311f6f0a1db516d06d97c06a3fa6d10e797cad578937a47b3992', + packageId: '0xed799c2fb6fc64bedc7df4fddb272af8718b2fe53a20b7c60ea927ad36b5b6ae', vault: '0x869f5100c0ecc0b35c7edad87ba3d488fd291bdba4a7aae84b70d188f440f393', }, pyth: { From 81b38ea25bd7b2af04933ad4cfe5b7359529c268 Mon Sep 17 00:00:00 2001 From: Nataly Date: Wed, 19 Aug 2026 01:17:37 +0300 Subject: [PATCH 12/12] chore: re-trigger CI