bns-v2-sdk
Version:
The official BNS V2 SDK for interacting with Stacks Blockchain
198 lines (177 loc) • 5.21 kB
text/typescript
import { BaseZonefileData, PriceFunction, ZonefileData } from "./interfaces";
import { ClarityValue, UIntCV, ListCV } from "@stacks/transactions";
import {
makeRandomPrivKey,
getAddressFromPrivateKey,
} from "@stacks/transactions";
import { CallbackFunction } from "./config";
function hasNoQueryOrFragment(urlString: string): boolean {
const url = new URL(urlString);
return !url.search && !url.hash;
}
function noUserInfo(urlString: string): boolean {
const url = new URL(urlString);
return !url.username && !url.password;
}
function isAllowedS3Domain(urlString: string): boolean {
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: string): boolean {
try {
const url = new URL(urlString);
return url.protocol === "https:";
} catch {
return false;
}
}
function hasJsonExtension(urlString: string): boolean {
const pathname = new URL(urlString).pathname.toLowerCase();
return pathname.endsWith(".json");
}
function isSafeDomain(urlString: string): boolean {
const url = new URL(urlString);
const forbiddenPatterns = [/^localhost$/, /^127\.0\.0\.1$/];
return !forbiddenPatterns.some((pattern) => pattern.test(url.hostname));
}
export function decodeFQN(fqdn: string): {
name: string;
namespace: string;
subdomain?: string;
} {
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],
};
}
export function parsePriceFunction(data: {
[key: string]: ClarityValue;
}): PriceFunction {
const buckets = (data["buckets"] as ListCV<UIntCV>).value;
return {
base: (data["base"] as UIntCV).value,
coefficient: (data["coeff"] as UIntCV).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"] as UIntCV).value,
noVowelDiscount: (data["no-vowel-discount"] as UIntCV).value,
};
}
export function asciiToUtf8(asciiCodes: string): string {
return asciiCodes
.split(",")
.map((code) => String.fromCharCode(parseInt(code.trim())))
.join("");
}
export function generateRandomAddress() {
const randomPrivateKey = makeRandomPrivKey();
const privateKeyString = randomPrivateKey;
const randomAddress = getAddressFromPrivateKey(privateKeyString);
return randomAddress;
}
export function parseZonefile(zonefileString: string): ZonefileData {
try {
const parsed = JSON.parse(zonefileString);
const baseData: BaseZonefileData = {
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,
} as ZonefileData;
}
return {
...baseData,
subdomains: parsed.subdomains || {},
} as ZonefileData;
} catch (error) {
console.error("Error parsing zonefile:", error);
return {
owner: "",
general: "",
twitter: "",
url: "",
nostr: "",
lightning: "",
btc: "",
subdomains: {},
} as ZonefileData;
}
}
export function createZonefileData(params: ZonefileData): ZonefileData {
const baseData: BaseZonefileData = {
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,
} as ZonefileData;
}
return {
...baseData,
subdomains: "subdomains" in params ? params.subdomains : {},
} as ZonefileData;
}
export function stringifyZonefile(zonefileData: ZonefileData): string {
return JSON.stringify(zonefileData);
}
export function addCallbacks<T>(
options: T,
onFinish?: CallbackFunction,
onCancel?: CallbackFunction
): T & { onFinish?: CallbackFunction; onCancel?: CallbackFunction } {
return { ...options, onFinish, onCancel };
}