Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
23ff8b0
feat(suins): support Pyth Hermes v2 and keyed endpoints [SUIP-1111]
John-Atha Jul 22, 2026
142da3b
refactor(suins): keep BCS state parsing for Pyth/Wormhole [SUIP-1111]
John-Atha Jul 22, 2026
97f7dc5
feat(suins): target Pyth Pro keyed endpoint and upgraded state [SUIP-…
John-Atha Jul 22, 2026
ac59f4e
Merge branch 'main' into giannisathanasiou/suip-1111-suins-sdk-update…
John-Atha Jul 22, 2026
c30f4ed
feat(suins): switch payments flow to Pro _pro entrypoints on testnet
John-Atha Jul 27, 2026
d159c2e
test(suins): drop mock-only PriceServiceConnection unit test
John-Atha Jul 27, 2026
378a7b7
refactor(suins): inline Pyth endpoint as a local var again
John-Atha Jul 27, 2026
1aa689b
Merge remote-tracking branch 'origin/main' into giannisathanasiou/sui…
John-Atha Jul 27, 2026
55ac1ef
feat(suins): throw early when pythAccessToken is missing
John-Atha Jul 28, 2026
d52164b
refactor(suins): make pythAccessToken readonly
John-Atha Jul 28, 2026
ffede08
fix(suins): guard the Hermes response shape in getLatestVaas
John-Atha Jul 28, 2026
106559f
docs(suins): drop stale testnet NS feed comment
John-Atha Jul 28, 2026
e9a3b38
[SUIP-1174] chore(suins): bump mainnet payments/bbb package ids
natalypeyioti Aug 18, 2026
81b38ea
chore: re-trigger CI
natalypeyioti Aug 18, 2026
d4c63be
fix(suins): point mainnet at SuiNS core v5 to fix InvalidLinkage
John-Atha Aug 20, 2026
46fd89f
Merge branch 'main' into giannisathanasiou/suip-1111-suins-sdk-update…
John-Atha Aug 20, 2026
886fb63
style(suins): reformat generated payments binding for prettier 3.9.6
John-Atha Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/suins-pyth-keyed-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/suins': major
---

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.
3 changes: 3 additions & 0 deletions packages/suins/.env.example
Original file line number Diff line number Diff line change
@@ -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=
18 changes: 10 additions & 8 deletions packages/suins/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ export const mainPackage: Config = {
},
payments: {
packageId: '0xdd0a4a34152a80d7841710e916a407b2a62961eee5b2188dcfdaa24194f66286',
packageIdV1: '0xdd0a4a34152a80d7841710e916a407b2a62961eee5b2188dcfdaa24194f66286',
},
bbb: {
packageId: '0x6268d072063a311f6f0a1db516d06d97c06a3fa6d10e797cad578937a47b3992',
vault: '0x869f5100c0ecc0b35c7edad87ba3d488fd291bdba4a7aae84b70d188f440f393',
},
pyth: {
pythStateId: '0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8',
wormholeStateId: '0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c',
pythStateId: '0x03719fae774ddab3cfcaa53bbc046f0cbe21410019b6280811bf3f9f4b05839d',
wormholeStateId: '0xdbca52b9fb4f712e25f61f974586d93ac541bcf8389564f0323bb07215168b5c',
},
coins: {
SUI: {
Expand Down Expand Up @@ -73,27 +74,28 @@ export const mainPackage: Config = {
packageId: '0x63029aae8abbefae4f4ac6c5e3e0021159ea93a94ba648681fd64caf5b40677a',
},
payments: {
packageId: '0xc391c200188dd1a363ff12dcffe07eaac5cf28ad1cd8dc0fcc18f2f8625f0da2',
packageId: '0x4f33a0e1e30530f2aa500a41b9e3d502f8af3ef2c20bd0a1e42374e329da7cb0',
packageIdV1: '0xc391c200188dd1a363ff12dcffe07eaac5cf28ad1cd8dc0fcc18f2f8625f0da2',
},
bbb: {
packageId: '0xed9b18147ca81c8f3f60192c8d0630574e42387cd200a6e39b3e4e07df1ce6e6',
packageId: '0xab7f8da0f974ae38c205d9351787ae938da65c0a0e81d9788014f5f62a917aa1',
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
Comment thread
John-Atha marked this conversation as resolved.
Outdated
NS: {
type: '0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTNS::TESTNS',
feed: '0x99137a18354efa7fb6840889d059fdb04c46a6ce21be97ab60d9ad93e91ac758',
feed: '0xbb5ff26e47a3a6cc7ec2fce1db996c2a145300edc5acaabe43bf9ff7c5dd5d32',
},
/// this is a test token published as 0xb48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTUSDC::TESTUSDC
USDC: {
Expand Down
126 changes: 100 additions & 26 deletions packages/suins/src/contracts/suins_payments/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,68 @@ export function handleBasePayment(options: HandleBasePaymentOptions) {
});
}
export interface HandlePaymentArguments {
Suins: RawTransactionArgument<string>;
BbbVault: RawTransactionArgument<string>;
Intent: TransactionArgument;
Payment: RawTransactionArgument<string>;
PriceInfoObject: RawTransactionArgument<string>;
UserPriceGuard: RawTransactionArgument<number | bigint>;
}
export interface HandlePaymentOptions {
package?: string;
arguments:
| HandlePaymentArguments
| [
Suins: RawTransactionArgument<string>,
BbbVault: RawTransactionArgument<string>,
Intent: TransactionArgument,
Payment: RawTransactionArgument<string>,
PriceInfoObject: RawTransactionArgument<string>,
UserPriceGuard: RawTransactionArgument<number | bigint>,
];
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<string>;
bbbVault: RawTransactionArgument<string>;
intent: TransactionArgument;
payment: RawTransactionArgument<string>;
priceInfoObject: RawTransactionArgument<string>;
userPriceGuard: RawTransactionArgument<number | bigint>;
}
export interface HandlePaymentOptions {
export interface HandlePaymentProOptions {
package?: string;
arguments:
| HandlePaymentArguments
| HandlePaymentProArguments
| [
suins: RawTransactionArgument<string>,
bbbVault: RawTransactionArgument<string>,
Expand All @@ -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
Expand All @@ -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<string>;
BaseAmount: RawTransactionArgument<number | bigint>;
PriceInfoObject: RawTransactionArgument<string>;
}
export interface CalculatePriceOptions {
package?: string;
arguments:
| CalculatePriceArguments
| [
Suins: RawTransactionArgument<string>,
BaseAmount: RawTransactionArgument<number | bigint>,
PriceInfoObject: RawTransactionArgument<string>,
];
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<string>;
baseAmount: RawTransactionArgument<number | bigint>;
priceInfoObject: RawTransactionArgument<string>;
}
export interface CalculatePriceOptions {
export interface CalculatePriceProOptions {
package?: string;
arguments:
| CalculatePriceArguments
| CalculatePriceProArguments
| [
suins: RawTransactionArgument<string>,
baseAmount: RawTransactionArgument<number | bigint>,
Expand All @@ -146,24 +225,19 @@ 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<SUI>(suins, intent, ...);
* 3. const coin = txb.splitCoins(baseCoin, [price])
* 4. handle_payment<SUI>(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'];
return (tx: Transaction) =>
tx.moveCall({
package: packageAddress,
module: 'payments',
function: 'calculate_price',
function: 'calculate_price_pro',
arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames),
typeArguments: options.typeArguments,
});
Expand Down
10 changes: 7 additions & 3 deletions packages/suins/src/pyth/PriceServiceConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type PriceFeedRequestConfig = {
export type PriceServiceConnectionConfig = {
timeout?: number;
httpRetries?: number;
accessToken?: string;
};
export class PriceServiceConnection {
private httpClient: AxiosInstance;
Expand All @@ -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,
Expand All @@ -38,11 +40,13 @@ export class PriceServiceConnection {
* @returns Array of base64 encoded VAAs.
*/
async getLatestVaas(priceIds: HexString[]): Promise<string[]> {
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;
Comment thread
John-Atha marked this conversation as resolved.
Outdated
}
}
17 changes: 11 additions & 6 deletions packages/suins/src/suins-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { PaymentsConfig } from './contracts/suins_payments/payments.js';
export type SuinsExtensionOptions<Name extends string = 'suins'> = {
name?: Name;
packageInfo?: PackageInfo;
/** Access token for the keyed Pyth Hermes endpoint. Sent as `Authorization: Bearer <token>`. */
pythAccessToken?: string;
};

/**
Expand All @@ -45,6 +47,7 @@ export type SuinsExtensionOptions<Name extends string = 'suins'> = {
export function suins<const Name extends string = 'suins'>({
name = 'suins' as Name,
packageInfo,
pythAccessToken,
}: SuinsExtensionOptions<Name> = {}) {
return {
name,
Expand All @@ -53,6 +56,7 @@ export function suins<const Name extends string = 'suins'>({
client,
network: client.network,
packageInfo,
pythAccessToken,
});
},
};
Expand All @@ -62,10 +66,12 @@ export class SuinsClient {
client: ClientWithCoreApi;
network: SuiClientTypes.Network;
config: PackageInfo;
pythAccessToken?: string;
Comment thread
John-Atha marked this conversation as resolved.
Outdated

constructor(config: SuinsClientConfig) {
this.client = config.client;
this.network = config.network || 'mainnet';
this.pythAccessToken = config.pythAccessToken;

if (config.packageInfo) {
this.config = config.packageInfo;
Expand Down Expand Up @@ -168,7 +174,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,
Expand Down Expand Up @@ -285,11 +291,10 @@ export class SuinsClient {
}

async getPriceInfoObject(tx: Transaction, feed: string, feeCoin?: TransactionObjectArgument) {
Comment thread
John-Atha marked this conversation as resolved.
const endpoint =
this.network === 'testnet'
? 'https://hermes-beta.pyth.network'
: 'https://hermes.pyth.network';
const connection = new SuiPriceServiceConnection(endpoint);
const endpoint = 'https://pyth.dourolabs.app/hermes';
Comment thread
John-Atha marked this conversation as resolved.
const connection = new SuiPriceServiceConnection(endpoint, {
accessToken: this.pythAccessToken,
});
const priceIDs = [feed];
const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIDs);

Expand Down
4 changes: 2 additions & 2 deletions packages/suins/src/suins-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/suins/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface PackageInfo {
};
payments: {
packageId: string;
packageIdV1: string;
};
bbb: {
packageId: string;
Expand Down Expand Up @@ -102,6 +103,8 @@ export type SuinsClientConfig = {
client: ClientWithCoreApi;
network?: SuiClientTypes.Network;
packageInfo?: PackageInfo;
/** Access token for the keyed Pyth Hermes endpoint. Sent as `Authorization: Bearer <token>`. */
pythAccessToken?: string;
};

export type SuinsPriceList = Map<[number, number], number>;
Expand Down
8 changes: 6 additions & 2 deletions packages/suins/test/live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
Expand All @@ -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)}`);
Expand Down
Loading
Loading