Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
}
}
23 changes: 17 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,16 @@ 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);
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';
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
Loading
Loading