UNPKG

@iota/iota-names-sdk

Version:
409 lines (408 loc) 14.1 kB
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