A library for interacting with BNS and BNSx. Learn more.
This library has a few main components:
BNSApiClient- an interface to the BNS APIBNSContractsClient- an interface to all BNS and BNSx contracts- A set of types and utility functions for working with BNS
- Interacting with the BNS API
- Interacting with BNS and BNSx contracts
- Zonefiles
- Utility functions
- Punycode
The default base URL for all API queries is https://api.bns.xyz.
import { BnsApiClient } from '@bns-x/client';
const bns = new BnsApiClient();
// optionally, set base URL:
// new BnsApiClient('http://example.com');Returns: string | null
The logic for returning a user's "display name" is:
- If the user owns any BNS Core names, return that name
- If the user has a subdomain, return that
- If the user owns a BNSx name, return that name
const address = 'SP123...';
const name = await bns.getDisplayName(address);You can call getNameDetails two different ways:
getNameDetails(name)- wherenameis a fully-qualified name (likeexample.btc)getNameDetails(name, namespace)
Returns: NameInfoResponse | null
const details = await bns.getNameDetails('example.btc');
// equivalent to:
// const details = await bns.getNameDetails('example', 'btc');If the name doesn't exist, the function returns null.
Returns:
address: the owner of this nameexpire_block: the block height this name expires atzonefile: zonefile for the nameisBnsx: a boolean indicating whether the name has migrated to BNSx
If the owner of the name has inscribed their zonefile, it also returns:
inscriptionId: the ID of the inscription containing the zonefileinscription: object containing:blockHeight: Bitcoin block height where the name was inscribedtimestamp: timestamp of the inscription's creation datetxid: Bitcoin txid where the inscription was createdsat: the "Sat" holding the inscription
If the name has been migrated to BNSx, this response also includes:
id: the NFT ID (integer)wrapper: the wrapper contract that owns this name
Returns: NamesByAddressResponse
If you want to fetch multiple names (both BNS and BNSx) owned by an address, you can use this function. Note that if you just want to show a name for an address, using getDisplayName will have better performance.
const allNames = await bns.getAddressNames(address);The return type has these properties:
namesarray of names (strings) the user ownsdisplayNamea single name to show for the user (seegetDisplayName)coreNamethe address's BNS Core name, if they have oneprimaryProperties: The properties of the address's primary BNSx name (seenameProperties)nameProperties: properties for the address's BNSx namesid: numerical ID of the namecombined: the full name (ieexample.btc)decoded: if the name is punycode, this will return the UTF-8 version of the namenameandnamespace: the separate parts of the name (ieexampleandbtcforexample.btc)
This package includes clarigen generated types and functions for interacting with BNS contracts.
Create a new client by specifying the network you're using. It can be one of mainnet, testnet, or devnet. This is used to automatically set the correct contract identifier for your network.
For calling read-only functions, you can also specify a Stacks API endpoint as the second parameter.
import { BnsContractsClient } from '@bns-x/client';
// defaults to "mainnet"
export const contracts = new BnsContractsClient();
// For other networks:
// new BnsContractsClient('testnet', 'https://stacks-node-api.testnet.stacks.co');The contracts client includes getters for various BNSx and BNS contracts:
registry: the main name registry contract for BNSxqueryHelper: a contract that exposes various query-related helpersbnsCore: the BNS Core contractupgrader: the contract responsible for upgrading wrapped names to BNSx
Refer to the clarigen docs for more information - but here are a few quick examples.
In each example, contracts refers to an instance of the BnsContractsClient.
Generate a ClarigenClient
import { ClarigenClient } from '@clarigen/web';
// Uses micro-stacks for network information
import { microStacksClient } from './micro-stacks';
export const clarigen = new Clarigen(microStacksClient);Call read-only functions
const primaryName = await clarigen.ro(contracts.registry.getPrimaryName(address));
// `roOk` is a helper to automatically expect and scope to a function's `ok` type
const price = await clarigen.roOk(contracts.bnsCore.getNamePrice(nameBuff, namespaceBuff));Make transactions
import { useOpenContractCall } from '@micro-stacks/react';
const registry = contracts.registry;
export const TransferName = () => {
const { openContractCall } = useOpenContractCall();
const nameId = 1n;
const makeTransfer = async () => {
await openContractCall({
...registry.transfer({
id: nameId,
sender: 'SP123...',
recipient: 'SP123...',
}),
// ... include other tx args
async onFinish(data) {
console.log('Broadcasted tx');
},
});
};
return <button onClick={() => makeTransfer()}>Transfer</button>;
};Generate a pre-order tx:
import { asciiToBytes, randomSalt, hashFqn } from '@bns-x/client';
const name = 'example';
const namespace = 'btc';
const price = 2000000n;
const salt = randomSalt();
const hashedFqn = hashFqn(name, namespace, salt);
const tx = contracts.bnsCore.namePreorder({
hashedSaltedFqn: hashedFqn,
stxToBurn: price,
});Later, register the name:
const register = contracts.bnsCore.nameRegister({
name: asciiToBytes(name),
namespace: asciiToBytes(namespace),
zonefileHash: new Uint8Array(),
salt,
});contracts.registry.transfer({
id: 1,
sender: 'SP123..',
recipient: 'SP123..',
});Because each wrapper contract is at a different address, the client exposes a helper function for creating a "wrapper instance" at a specific address.
const contractId = 'SP123...xyz.name-wrapper-200';
const wrapperContract = contracts.nameWrapper(contractId);
// now can interact with its functions
// wrapperContract.unwrap(...)This example uses both the API and contracts client.
const nameDetails = await bnsApi.getNameDetailsFromFqn('example.btc');
if (!nameDetails.isBnsx) throw new Error('Cant unwrap name');
const { wrapper } = nameDetails;
const wrapperContract = contracts.nameWrapper(wrapper);
// you can specify a different recipient for the unwrapped name.
// If not specified, it defaults to the owner of the BNSx name.
wrapperContract.unwrap(); // sends BNS name to current BNSx owner
// send to different address:
wrapperContract.unwrap({
recipient: 'SP123...asdf',
});If you need to deploy a name wrapper contract, you can get the source code from nameWrapperCode.
const code = contracts.nameWrapperCode();This library exposes a few functions to make it easier to get records from a name's zonefile.
The ZoneFile class can be constructed with a zonefile (string) and can be used to easily get information from the zonefile.
import { ZoneFile, BnsApiClient } from '@bns-x/client';
const client = new BnsApiClient();
// Returns `string | null`;
export async function getBtcAddress(name: string) {
const nameDetails = await client.getNameDetailsFromFqn(name);
if (nameDetails === null) {
// name not found
return null;
}
const zonefile = new ZoneFile(nameDetails.zonefile);
// Returns `null` if `_btc._addr` not found in zonefile
return zonefile.btcAddr;
}If you want to get the TXT record for any specific key, you can use getTxtRecord.
const zonefile = new ZoneFile(nameDetails.zonefile);
const txtValue = zonefile.getTxtRecord('_eth._addr'); // returns `string | null`This library exposes a few utility functions that come in handy when working with BNS.
In BNS, all names are stored on-chain as ascii-converted bytes.
import { asciiToBytes, bytesToAscii } from '@bns-x/client';
// the human-readable version of the name:
const name = 'example';
// the name stored on chain
const nameBytes = asciiToBytes(name);
// convert from on-chain:
bytesToAscii(nameBytes) === name;When preordering a name on BNS, you need to create a random salt.
import { randomSalt } from '@bns-x/client';
const salt = randomSalt(); // Uint8ArrayWhen preordering a name, you need to create a "hashed salted fully qualified name". This helper function generates that for you.
import { asciiToBytes, randomSalt, hashFqn } from '@bns-x/client';
const name = 'example';
const namespace = 'btc';
const salt = randomSalt();
const hashedFqn = hashFqn(name, namespace, salt);If you have a string, you can parse it into individual parts:
import { parseFqn } from '@bns-x/client';
const name = parseFqn('example.btc');
name.name; // 'example'
name.namespace; // 'btc'
name.subdomain; // undefined
parseFqn('sub.example.btc');
// { name: 'exampl
5EE9
e', namespace: 'btc', subdomain: 'sub' }Helper function to expose namespaces that do not expire.
Note: This is a hard-coded list. If new namespaces are registered, they are not automatically added to this list.
If you want to fetch on-chain data, use BnsContractsClient#fetchNamespaceExpiration.
Also exposed is NO_EXPIRATION_NAMESPACES, which is a set of strings.
import { doesNamespaceExpire, NO_EXPIRATION_NAMESPACES } from '@bns-x/client';
doesNamespaceExpire('stx'); // returns false
NO_EXPIRATION_NAMESPACE.has('stx'); // returns trueThis package includes a few punycode-related functions and utilities. Note: if you only want the punycode functions, you can import them from @bns-x/punycode.
Under the hood, the @adraffy/punycode library is used.
Converts a punycode string to unicode.
import { toUnicode } from '@bns-x/client';
toUnicode('xn--1ug66vku9r8p9h.btc'); // returns 'π§ββοΈ.btc'Convert a unicode string to punycode.
import { toPunycode } from '@bns-x/client';
toPunycode('π§ββοΈ.btc'); // returns 'xn--1ug66vku9r8p9h.btc'In Emoji, there are various "zero-width" or invisible characters that are part of a valid "emoji sequence". However, some users add invalid ZWJ characters to a name in order to try and trick other users into thinking that a name just a single emoji.
This library exposes some functions for determining whether a string contains extra invalid ZWJ characters. It will not flag valid ZWJ sequence emojis.
import { hasInvalidExtraZwj } from '@bns-x/client';
const badString = 'π§π»β'; // {1F9DC}{1F3FB}{200D} - extra `200D` at end
hasInvalidExtraZwj(badString); // true
const goodString = 'π§ββοΈ'; // {1F9D4}{200D}{2642}{FE0F}
hasInvalidExtraZwj(goodString); // false, even though there are ZWJ charactersFor apps like marketplaces that want to show both a punycode and unicode name, as well as flag if there is an invalid ZWJ modifier, fullDisplayName creates a string that is appropriate for regular, punycode, and invalid punycode names.
import { fullDisplayName } from '@bns-x/client';
// regular names:
fullDisplayName('example.btc'); // "example.btc"
// punycode names:
fullDisplayName('xn--1ug66vku9r8p9h.btc'); // 'xn--1ug66vku9r8p9h.btc (π§ββοΈ.btc)'
// punycode with extra ZWJ
fullDisplayName('xn--1ug2145p8xd.btc'); // 'xn--1ug2145p8xd.btc (π§π»β.btcπ₯)'