bns-v2-sdk
Version:
The official BNS V2 SDK for interacting with Stacks Blockchain
341 lines (340 loc) • 11.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeFQN = decodeFQN;
exports.parsePriceFunction = parsePriceFunction;
exports.asciiToUtf8 = asciiToUtf8;
exports.generateRandomAddress = generateRandomAddress;
exports.parseZonefile = parseZonefile;
exports.createZonefileData = createZonefileData;
exports.stringifyZonefile = stringifyZonefile;
exports.addCallbacks = addCallbacks;
exports.createFormattedZonefileData = createFormattedZonefileData;
const transactions_1 = require("@stacks/transactions");
function hasNoQueryOrFragment(urlString) {
const url = new URL(urlString);
return !url.search && !url.hash;
}
function noUserInfo(urlString) {
const url = new URL(urlString);
return !url.username && !url.password;
}
function isAllowedS3Domain(urlString) {
try {
const url = new URL(urlString);
const s3DomainPattern = /^[a-z0-9.-]+\.s3([.-][a-z0-9-]+)*\.amazonaws\.com$/i;
return s3DomainPattern.test(url.hostname);
}
catch {
return false;
}
}
function isValidHttpsUrl(urlString) {
try {
const url = new URL(urlString);
return url.protocol === "https:";
}
catch {
return false;
}
}
function hasJsonExtension(urlString) {
const pathname = new URL(urlString).pathname.toLowerCase();
return pathname.endsWith(".json");
}
function isSafeDomain(urlString) {
const url = new URL(urlString);
const forbiddenPatterns = [/^localhost$/, /^127\.0\.0\.1$/];
return !forbiddenPatterns.some((pattern) => pattern.test(url.hostname));
}
function decodeFQN(fqdn) {
const nameParts = fqdn.split(".");
if (nameParts.length > 2) {
return {
subdomain: nameParts[0],
name: nameParts[1],
namespace: nameParts[2],
};
}
return {
name: nameParts[0],
namespace: nameParts[1],
};
}
function parsePriceFunction(data) {
const buckets = data["buckets"].value;
return {
base: data["base"].value,
coefficient: data["coeff"].value,
b1: buckets[0].value,
b2: buckets[1].value,
b3: buckets[2].value,
b4: buckets[3].value,
b5: buckets[4].value,
b6: buckets[5].value,
b7: buckets[6].value,
b8: buckets[7].value,
b9: buckets[8].value,
b10: buckets[9].value,
b11: buckets[10].value,
b12: buckets[11].value,
b13: buckets[12].value,
b14: buckets[13].value,
b15: buckets[14].value,
b16: buckets[15].value,
nonAlphaDiscount: data["nonalpha-discount"].value,
noVowelDiscount: data["no-vowel-discount"].value,
};
}
function asciiToUtf8(asciiCodes) {
return asciiCodes
.split(",")
.map((code) => String.fromCharCode(parseInt(code.trim())))
.join("");
}
function generateRandomAddress() {
const randomPrivateKey = (0, transactions_1.makeRandomPrivKey)();
const privateKeyString = randomPrivateKey;
const randomAddress = (0, transactions_1.getAddressFromPrivateKey)(privateKeyString);
return randomAddress;
}
function parseZonefile(zonefileString) {
try {
const parsed = JSON.parse(zonefileString);
const baseData = {
owner: parsed.owner || "",
general: parsed.general || "",
twitter: parsed.twitter || "",
url: parsed.url || "",
nostr: parsed.nostr || "",
lightning: parsed.lightning || "",
btc: parsed.btc || "",
};
if (parsed.externalSubdomainFile) {
return {
...baseData,
externalSubdomainFile: parsed.externalSubdomainFile,
};
}
return {
...baseData,
subdomains: parsed.subdomains || {},
};
}
catch (error) {
console.error("Error parsing zonefile:", error);
return {
owner: "",
general: "",
twitter: "",
url: "",
nostr: "",
lightning: "",
btc: "",
subdomains: {},
};
}
}
function createZonefileData(params) {
const baseData = {
owner: params.owner,
general: params.general || "",
twitter: params.twitter || "",
url: params.url || "",
nostr: params.nostr || "",
lightning: params.lightning || "",
btc: params.btc || "",
};
if ("externalSubdomainFile" in params && params.externalSubdomainFile) {
const fileUrl = params.externalSubdomainFile;
if (!isValidHttpsUrl(fileUrl) ||
!hasJsonExtension(fileUrl) ||
!isSafeDomain(fileUrl) ||
!isAllowedS3Domain(fileUrl) ||
!hasNoQueryOrFragment(fileUrl) ||
!noUserInfo(fileUrl)) {
throw new Error("Invalid externalSubdomainFile URL");
}
return {
...baseData,
externalSubdomainFile: fileUrl,
};
}
return {
...baseData,
subdomains: "subdomains" in params ? params.subdomains : {},
};
}
function stringifyZonefile(zonefileData) {
return JSON.stringify(zonefileData);
}
function addCallbacks(options, onFinish, onCancel) {
return { ...options, onFinish, onCancel };
}
function isValidImageUrl(url) {
try {
const validExtensions = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
const urlObj = new URL(url);
const pathname = urlObj.pathname.toLowerCase();
return validExtensions.some((ext) => pathname.endsWith(ext));
}
catch {
return false;
}
}
function isValidUrl(url) {
try {
new URL(url);
return true;
}
catch {
return false;
}
}
function validateSocialEntry(social) {
return (typeof social.platform === "string" &&
typeof social.username === "string" &&
social.platform.trim() !== "" &&
social.username.trim() !== "");
}
function validateAddressEntry(address) {
return (typeof address.network === "string" &&
typeof address.address === "string" &&
typeof address.type === "string" &&
address.network.trim() !== "" &&
address.address.trim() !== "" &&
address.type.trim() !== "");
}
function validateMetaEntry(meta) {
return (typeof meta.name === "string" &&
typeof meta.value === "string" &&
meta.name.trim() !== "");
}
function validateSubdomainEntry(subdomain) {
if (!subdomain.owner ||
typeof subdomain.owner !== "string" ||
subdomain.owner.trim() === "") {
return false;
}
if (subdomain.pfp &&
(!isValidUrl(subdomain.pfp) || !isValidImageUrl(subdomain.pfp))) {
return false;
}
if (subdomain.website && !isValidUrl(subdomain.website)) {
return false;
}
if (subdomain.social && !Array.isArray(subdomain.social)) {
return false;
}
if (subdomain.social) {
for (const social of subdomain.social) {
if (!validateSocialEntry(social)) {
return false;
}
}
}
if (subdomain.addresses && !Array.isArray(subdomain.addresses)) {
return false;
}
if (subdomain.addresses) {
for (const address of subdomain.addresses) {
if (!validateAddressEntry(address)) {
return false;
}
}
}
return true;
}
function createFormattedZonefileData(params) {
if (!params.owner ||
typeof params.owner !== "string" ||
params.owner.trim() === "") {
throw new Error("Owner field is required and must be a non-empty string");
}
if (params.pfp && (!isValidUrl(params.pfp) || !isValidImageUrl(params.pfp))) {
throw new Error("pfp must be a valid image URL with supported format (.png, .jpg, .jpeg, .gif, .svg, .webp)");
}
if (params.website && !isValidUrl(params.website)) {
throw new Error("website must be a valid URL");
}
if (params.social) {
if (!Array.isArray(params.social)) {
throw new Error("social must be an array");
}
for (const social of params.social) {
if (!validateSocialEntry(social)) {
throw new Error("Invalid social entry: platform and username are required");
}
}
}
if (params.addresses) {
if (!Array.isArray(params.addresses)) {
throw new Error("addresses must be an array");
}
for (const address of params.addresses) {
if (!validateAddressEntry(address)) {
throw new Error("Invalid address entry: network, address, and type are required");
}
}
}
if (params.meta) {
if (!Array.isArray(params.meta)) {
throw new Error("meta must be an array");
}
for (const meta of params.meta) {
if (!validateMetaEntry(meta)) {
throw new Error("Invalid meta entry: name and value are required");
}
}
}
if (params.subdomains) {
if (!Array.isArray(params.subdomains)) {
throw new Error("subdomains must be an array");
}
for (const subdomainMap of params.subdomains) {
if (typeof subdomainMap !== "object" || subdomainMap === null) {
throw new Error("Each subdomain entry must be an object");
}
for (const [subdomainName, subdomainData] of Object.entries(subdomainMap)) {
if (!validateSubdomainEntry(subdomainData)) {
throw new Error(`Invalid subdomain entry for ${subdomainName}`);
}
}
}
}
if (params.externalSubdomainsFile) {
const fileUrl = params.externalSubdomainsFile;
if (!isValidHttpsUrl(fileUrl) ||
!hasJsonExtension(fileUrl) ||
!isSafeDomain(fileUrl) ||
!hasNoQueryOrFragment(fileUrl) ||
!noUserInfo(fileUrl)) {
throw new Error("Invalid externalSubdomainsFile URL");
}
}
const formattedData = {
owner: params.owner,
};
if (params.btc)
formattedData.btc = params.btc;
if (params.bio)
formattedData.bio = params.bio;
if (params.website)
formattedData.website = params.website;
if (params.pfp)
formattedData.pfp = params.pfp;
if (params.name)
formattedData.name = params.name;
if (params.location)
formattedData.location = params.location;
if (params.social && params.social.length > 0)
formattedData.social = params.social;
if (params.addresses && params.addresses.length > 0)
formattedData.addresses = params.addresses;
if (params.meta && params.meta.length > 0)
formattedData.meta = params.meta;
if (params.subdomains && params.subdomains.length > 0)
formattedData.subdomains = params.subdomains;
if (params.externalSubdomainsFile)
formattedData.externalSubdomainsFile = params.externalSubdomainsFile;
return formattedData;
}