@iota/iota-names-sdk
Version:
IOTA-Names SDK
409 lines (408 loc) • 14.1 kB
JavaScript
import { bcs } from "@iota/iota-sdk/bcs";
import { graphql } from "@iota/iota-sdk/graphql/schemas/2025.2";
import { fromB64, toB64 } from "@iota/iota-sdk/utils";
import { blake2b } from "@noble/hashes/blake2";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { CouponBcs, CouponHouseBcs, DummyFieldBcs, NameBcs } from "./bcs.js";
import { ALLOWED_METADATA, packages } from "./constants.js";
import { applyCouponsToPrice, validateCoupons } from "./coupons.js";
import {
getConfigType,
getCoreConfigType,
getNameType,
getPricelistConfigType,
getRenewalPricelistConfigType,
isSubname,
validateYears
} from "./helpers.js";
import { isValidIotaName, normalizeIotaName, validateIotaName } from "./utils.js";
class IotaNamesClient {
constructor(config) {
this.graphQlClient = config.graphQlClient;
if ("network" in config) {
this.config = packages[config.network];
} else {
this.config = config.packageInfo;
}
}
/**
* Returns the core config of IOTA Names.
*/
async getCoreConfig() {
if (!this.config.iotaNamesObjectId) throw new Error("IotaNames object ID is not set");
if (!this.config.packageId) throw new Error("IotaNames package ID is not set");
const coreConfigBcsB64 = toB64(
DummyFieldBcs.serialize({
dummy_field: false
}).toBytes()
);
const coreConfigResponse = await this.graphQlClient.query({
query: graphql(`
query getCoreConfig($parentId: IotaAddress!, $name: DynamicFieldName!) {
owner(address: $parentId) {
address
dynamicField(name: $name) {
value {
... on MoveValue {
json
}
}
}
}
}
`),
variables: {
parentId: this.config.iotaNamesObjectId,
name: {
type: getConfigType(
this.config.packageId,
getCoreConfigType(this.config.packageId)
),
bcs: coreConfigBcsB64
}
}
});
const coreConfig = coreConfigResponse?.data?.owner?.dynamicField?.value?.json;
if (!coreConfig) {
throw new Error("Core config not found or is invalid");
}
return coreConfig;
}
/**
* Returns the price list for IOTA names in the base asset.
*/
// Format:
// {
// [ 3, 3 ] => 500000000,
// [ 4, 4 ] => 100000000,
// [ 5, 63 ] => 20000000
// }
async getPriceList() {
if (!this.config.iotaNamesObjectId) throw new Error("IotaNames object ID is not set");
if (!this.config.packageId) throw new Error("IotaNames package ID is not set");
const pricingConfigBcsB64 = toB64(
DummyFieldBcs.serialize({
dummy_field: false
}).toBytes()
);
const priceListResponse = await this.graphQlClient.query({
query: graphql(`
query getPriceList($parentId: IotaAddress!, $name: DynamicFieldName!) {
owner(address: $parentId) {
address
dynamicField(name: $name) {
value {
... on MoveValue {
json
}
}
}
}
}
`),
variables: {
parentId: this.config.iotaNamesObjectId,
name: {
type: getConfigType(
this.config.packageId,
getPricelistConfigType(this.config.packageId)
),
bcs: pricingConfigBcsB64
}
}
});
const priceList = priceListResponse?.data?.owner?.dynamicField?.value?.json?.pricing;
const contents = priceList?.contents;
if (!contents) {
throw new Error("Price list not found or content is invalid");
}
const priceMap = /* @__PURE__ */ new Map();
for (const entry of contents) {
const { pos0, pos1 } = entry.key;
const key = [Number(pos0), Number(pos1)];
const value = Number(entry.value);
priceMap.set(key, value);
}
return priceMap;
}
/**
* Returns the renewal price list for IOTA names in the base asset.
*/
// Format:
// {
// [ 3, 3 ] => 500000000,
// [ 4, 4 ] => 100000000,
// [ 5, 63 ] => 20000000
// }
async getRenewalPriceList() {
if (!this.config.iotaNamesObjectId) throw new Error("IotaNames object ID is not set");
if (!this.config.packageId) throw new Error("IotaNames package ID is not set");
const pricingConfigBcsB64 = toB64(
DummyFieldBcs.serialize({
dummy_field: false
}).toBytes()
);
const priceListResponse = await this.graphQlClient.query({
query: graphql(`
query getRenewalPriceList($parentId: IotaAddress!, $name: DynamicFieldName!) {
owner(address: $parentId) {
address
dynamicField(name: $name) {
value {
... on MoveValue {
json
}
}
}
}
}
`),
variables: {
parentId: this.config.iotaNamesObjectId,
name: {
type: getConfigType(
this.config.packageId,
getRenewalPricelistConfigType(this.config.packageId)
),
bcs: pricingConfigBcsB64
}
}
});
const priceList = priceListResponse?.data?.owner?.dynamicField?.value?.json?.config?.pricing;
const contents = priceList?.contents;
if (!contents) {
throw new Error("Price list not found or content is invalid");
}
const priceMap = /* @__PURE__ */ new Map();
for (const entry of contents) {
const { pos0, pos1 } = entry.key;
const key = [Number(pos0), Number(pos1)];
const value = Number(entry.value);
priceMap.set(key, value);
}
return priceMap;
}
async getDefaultName(address) {
const response = await this.graphQlClient.query({
query: graphql(`
query resolveNameServiceName($address: IotaAddress!, $nameFormat: NameFormat) {
address(address: $address) {
iotaNamesDefaultName(format: $nameFormat)
}
}
`),
variables: {
address
}
});
const defaultName = response?.data?.address?.iotaNamesDefaultName ?? null;
return defaultName;
}
async getNameRecord(name) {
if (!isValidIotaName(name)) throw new Error("Invalid IOTA name");
if (!this.config.registryTableId) throw new Error("IotaNames package ID is not set");
const nameBcsB64 = toB64(
NameBcs.serialize({
labels: normalizeIotaName(name, "dot").split(".").reverse()
}).toBytes()
);
const nameRecordResponse = await this.graphQlClient.query({
query: graphql(`
query getNameRecord($parentId: IotaAddress!, $name: DynamicFieldName!) {
owner(address: $parentId) {
address
dynamicField(name: $name) {
value {
... on MoveValue {
json
}
}
}
}
}
`),
variables: {
parentId: this.config.registryTableId,
name: {
type: getNameType(this.config.packageId),
bcs: nameBcsB64
}
}
});
const nameRecord = nameRecordResponse.data?.owner?.dynamicField?.value?.json;
if (!nameRecord) return null;
const nameRecordData = nameRecord.data?.contents;
if (nameRecord.error || !nameRecordData)
throw new Error("Name record not found. This name is not registered.");
const data = {};
if (nameRecordData) {
nameRecordData.forEach((field) => {
if (field.key) {
data[field.key] = field.value;
}
});
}
return {
name,
nftId: nameRecord?.nft_id,
targetAddress: nameRecord?.target_address,
expirationTimestampMs: Number(nameRecord?.expiration_timestamp_ms),
data,
avatar: data[ALLOWED_METADATA.avatar]
};
}
async getCouponHouse() {
if (!this.config.iotaNamesObjectId) throw new Error("IotaNames object ID is not set");
if (!this.config.packageId) throw new Error("IotaNames package ID is not set");
if (!this.config.couponsPackageId) throw new Error("Coupon package ID is not set");
const iotaNamesObjectId = this.config.iotaNamesObjectId;
const packageId = this.config.packageId;
const couponsPackageId = this.config.couponsPackageId;
const DummyFieldB64 = DummyFieldBcs.serialize({ dummy_field: false }).toBase64();
const couponHouseResponse = await this.graphQlClient.query({
query: graphql(`
query getIotaNamesCouponHouseRegistryKey(
$parentId: IotaAddress!
$name: DynamicFieldName!
) {
owner(address: $parentId) {
dynamicField(name: $name) {
value {
... on MoveValue {
bcs
}
}
}
}
}
`),
variables: {
parentId: iotaNamesObjectId,
name: {
type: `${packageId}::iota_names::RegistryKey<${couponsPackageId}::coupon_house::CouponHouse>`,
bcs: DummyFieldB64
}
}
});
const couponsHouseDynamicFieldBcsValue = couponHouseResponse?.data?.owner?.dynamicField?.value?.bcs;
if (!couponsHouseDynamicFieldBcsValue) {
throw new Error("Coupon house not found or is invalid");
}
return CouponHouseBcs.parse(fromB64(couponsHouseDynamicFieldBcsValue));
}
async resolveCoupon(couponCode) {
const couponHouse = await this.getCouponHouse();
const couponsTableId = couponHouse?.coupons?.coupons?.id.id.bytes;
if (!couponsTableId) {
throw new Error("Coupons table ID not found in the coupon house");
}
const couponCodeHash = bytesToHex(blake2b(couponCode, { dkLen: 32 }));
const couponCodeBytes = hexToBytes(couponCodeHash);
const couponCodeB64 = bcs.vector(bcs.u8()).serialize(couponCodeBytes).toBase64();
const couponResponse = await this.graphQlClient.query({
query: graphql(`
query getCouponBcs($parentId: IotaAddress!, $name: DynamicFieldName!) {
owner(address: $parentId) {
dynamicField(name: $name) {
value {
... on MoveValue {
bcs
}
}
}
}
}
`),
variables: {
parentId: couponsTableId,
name: {
type: "vector<u8>",
bcs: couponCodeB64
}
}
});
const couponBcsBase64 = couponResponse?.data?.owner?.dynamicField?.value?.bcs;
if (!couponBcsBase64) {
return null;
}
const couponData = CouponBcs.parse(fromB64(couponBcsBase64));
return { ...couponData, couponCode };
}
/**
* Calculates the registration or renewal price for an SLN (Second Level Name).
* It expects a name, the number of years and a `IotaNamesPriceList` object,
* as returned from `iotaNamesClient.getPriceList()` function, or `iotaNames.getRenewalPriceList()` function.
*
* It throws an error:
* 1. if the name is a subname
* 2. if the name is not a valid IOTA name
* 3. if the years are not between 1 and 5
*/
async calculatePrice({
name,
years,
isRegistration = true
}) {
if (!isValidIotaName(name)) {
throw new Error("Invalid IOTA names");
}
validateYears(years);
if (isSubname(name)) {
throw new Error("Subnames do not have a registration fee");
}
const length = normalizeIotaName(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 calculateDiscountedPrice({
coupons,
name,
years,
isRegistration = true,
address
}) {
if (coupons.every((coupon) => typeof coupon === "string")) {
const couponPromises = coupons.map(async (couponCode) => {
const coupon = await this.resolveCoupon(couponCode);
if (!coupon) {
throw new Error(`Coupon not found: ${couponCode}`);
}
return coupon;
});
coupons = await Promise.all(couponPromises);
}
const normalizedName = normalizeIotaName(name, "dot");
validateIotaName(normalizedName);
const nameParts = normalizedName.split(".");
const firstNamePart = nameParts[0];
validateCoupons(coupons, years, firstNamePart.length, address);
const standardPrice = await this.calculatePrice({
name,
years,
isRegistration
});
return applyCouponsToPrice(coupons, standardPrice);
}
}
export {
IotaNamesClient
};
//# sourceMappingURL=iota-names-client.js.map