@mysten/suins
Version:
244 lines (243 loc) • 9.55 kB
JavaScript
import { isValidSuiNSName, normalizeSuiNSName } from "@mysten/sui/utils";
import { mainPackage } from "./constants.js";
import {
getCoinDiscountConfigType,
getConfigType,
getDomainType,
getPricelistConfigType,
getRenewalPricelistConfigType,
isSubName,
validateYears
} from "./helpers.js";
import { SuiPriceServiceConnection, SuiPythClient } from "./pyth/pyth.js";
class SuinsClient {
constructor(config) {
this.client = config.client;
this.network = config.network || "mainnet";
if (this.network === "mainnet") {
this.config = mainPackage.mainnet;
} else if (this.network === "testnet") {
this.config = mainPackage.testnet;
} else {
throw new Error("Invalid network");
}
}
/**
* Returns the price list for SuiNS names in the base asset.
*/
// Format:
// {
// [ 3, 3 ] => 500000000,
// [ 4, 4 ] => 100000000,
// [ 5, 63 ] => 20000000
// }
async getPriceList() {
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 priceList = await this.client.getDynamicFieldObject({
parentId: this.config.suins,
name: {
type: getConfigType(
this.config.packageIdV1,
getPricelistConfigType(this.config.packageIdPricing)
),
value: { dummy_field: false }
}
});
if (!priceList?.data?.content || priceList.data.content.dataType !== "moveObject" || !("fields" in priceList.data.content)) {
throw new Error("Price list not found or content is invalid");
}
const fields = priceList.data.content.fields;
if (!fields.value || !fields.value.fields || !fields.value.fields.pricing) {
throw new Error("Pricing fields not found in the price list");
}
const contentArray = fields.value.fields.pricing.fields.contents;
const priceMap = /* @__PURE__ */ new Map();
for (const entry of contentArray) {
const keyFields = entry.fields.key.fields;
const key = [Number(keyFields.pos0), Number(keyFields.pos1)];
const value = Number(entry.fields.value);
priceMap.set(key, value);
}
return priceMap;
}
/**
* Returns the renewal price list for SuiNS names in the base asset.
*/
// Format:
// {
// [ 3, 3 ] => 500000000,
// [ 4, 4 ] => 100000000,
// [ 5, 63 ] => 20000000
// }
async getRenewalPriceList() {
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 priceList = await this.client.getDynamicFieldObject({
parentId: this.config.suins,
name: {
type: getConfigType(
this.config.packageIdV1,
getRenewalPricelistConfigType(this.config.packageIdPricing)
),
value: { dummy_field: false }
}
});
if (!priceList || !priceList.data || !priceList.data.content || priceList.data.content.dataType !== "moveObject" || !("fields" in priceList.data.content)) {
throw new Error("Price list not found or content structure is invalid");
}
const fields = priceList.data.content.fields;
if (!fields.value || !fields.value.fields || !fields.value.fields.config || !fields.value.fields.config.fields.pricing || !fields.value.fields.config.fields.pricing.fields.contents) {
throw new Error("Pricing fields not found in the price list");
}
const contentArray = fields.value.fields.config.fields.pricing.fields.contents;
const priceMap = /* @__PURE__ */ new Map();
for (const entry of contentArray) {
const keyFields = entry.fields.key.fields;
const key = [Number(keyFields.pos0), Number(keyFields.pos1)];
const value = Number(entry.fields.value);
priceMap.set(key, value);
}
return priceMap;
}
/**
* Returns the coin discount list for SuiNS names.
*/
// Format:
// {
// 'b48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTUSDC::TESTUSDC' => 0,
// '0000000000000000000000000000000000000000000000000000000000000002::sui::SUI' => 0,
// 'b48aac3f53bab328e1eb4c5b3c34f55e760f2fb3f2305ee1a474878d80f650f0::TESTNS::TESTNS' => 25
// }
async getCoinTypeDiscount() {
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 dfValue = await this.client.getDynamicFieldObject({
parentId: this.config.suins,
name: {
type: getConfigType(
this.config.packageIdV1,
getCoinDiscountConfigType(this.config.payments.packageId)
),
value: { dummy_field: false }
}
});
if (!dfValue || !dfValue.data || !dfValue.data.content || dfValue.data.content.dataType !== "moveObject" || !("fields" in dfValue.data.content)) {
throw new Error("dfValue not found or content structure is invalid");
}
const fields = dfValue.data.content.fields;
if (!fields.value || !fields.value.fields || !fields.value.fields.base_currency || !fields.value.fields.base_currency.fields || !fields.value.fields.base_currency.fields.name || !fields.value.fields.currencies || !fields.value.fields.currencies.fields || !fields.value.fields.currencies.fields.contents) {
throw new Error("Required fields are missing in dfValue");
}
const content = fields.value.fields;
const currencyDiscounts = content.currencies.fields.contents;
const discountMap = /* @__PURE__ */ new Map();
for (const entry of currencyDiscounts) {
const key = entry.fields.key.fields.name;
const value = Number(entry.fields.value.fields.discount_percentage);
discountMap.set(key, value);
}
return discountMap;
}
async getNameRecord(name) {
if (!isValidSuiNSName(name)) throw new Error("Invalid SuiNS name");
if (!this.config.registryTableId) throw new Error("Suins package ID is not set");
const nameRecord = await this.client.getDynamicFieldObject({
parentId: this.config.registryTableId,
name: {
type: getDomainType(this.config.packageIdV1),
value: normalizeSuiNSName(name, "dot").split(".").reverse()
}
});
const fields = nameRecord.data?.content;
if (nameRecord.error?.code === "dynamicFieldNotFound") return null;
if (nameRecord.error || !fields || fields.dataType !== "moveObject")
throw new Error("Name record not found. This domain is not registered.");
const content = fields.fields;
const data = {};
content.value.fields.data.fields.contents.forEach((item) => {
data[item.fields.key] = item.fields.value;
});
return {
name,
nftId: content.value.fields?.nft_id,
targetAddress: content.value.fields?.target_address,
expirationTimestampMs: content.value.fields?.expiration_timestamp_ms,
data,
avatar: data.avatar,
contentHash: data.content_hash,
walrusSiteId: data.walrus_site_id
};
}
/**
* Calculates the registration or renewal price for an SLD (Second Level Domain).
* It expects a domain name, the number of years and a `SuinsPriceList` object,
* as returned from `suinsClient.getPriceList()` function, or `suins.getRenewalPriceList()` function.
*
* It throws an error:
* 1. if the name is a subdomain
* 2. if the name is not a valid SuiNS name
* 3. if the years are not between 1 and 5
*/
async calculatePrice({
name,
years,
isRegistration = true
}) {
if (!isValidSuiNSName(name)) {
throw new Error("Invalid SuiNS name");
}
validateYears(years);
if (isSubName(name)) {
throw new Error("Subdomains do not have a registration fee");
}
const length = normalizeSuiNSName(name, "dot").split(".")[0].length;
const priceList = await this.getPriceList();
const renewalPriceList = await this.getRenewalPriceList();
let yearsRemain = years;
let price = 0;
if (isRegistration) {
for (const [[minLength, maxLength], pricePerYear] of priceList.entries()) {
if (length >= minLength && length <= maxLength) {
price += pricePerYear;
yearsRemain -= 1;
break;
}
}
}
for (const [[minLength, maxLength], pricePerYear] of renewalPriceList.entries()) {
if (length >= minLength && length <= maxLength) {
price += yearsRemain * pricePerYear;
break;
}
}
return price;
}
async getPriceInfoObject(tx, feed) {
const endpoint = this.network === "testnet" ? "https://hermes-beta.pyth.network" : "https://hermes.pyth.network";
const connection = new SuiPriceServiceConnection(endpoint);
const priceIDs = [
feed
// ASSET/USD price ID
];
const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIDs);
const wormholeStateId = this.config.pyth.wormholeStateId;
const pythStateId = this.config.pyth.pythStateId;
const client = new SuiPythClient(this.client, pythStateId, wormholeStateId);
return await client.updatePriceFeeds(tx, priceUpdateData, priceIDs);
}
async getObjectType(objectId) {
const objectResponse = await this.client.getObject({
id: objectId,
options: { showType: true }
});
if (objectResponse && objectResponse.data && objectResponse.data.type) {
return objectResponse.data.type;
}
throw new Error(`Type information not found for object ID: ${objectId}`);
}
}
export {
SuinsClient
};
//# sourceMappingURL=suins-client.js.map