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)
- wherename
is 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:
names
array of names (strings) the user ownsdisplayName
a single name to show for the user (seegetDisplayName
)coreName
the 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 namename
andnamespace
: the separate parts of the name (ieexample
andbtc
forexample.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(); // Uint8Array
When 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: 'example', 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 true
This 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 characters
For 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🟥)'