@bns-x/client
The BNS client is a library for Javascript developers who want to build BNS apps.
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
The default base URL for all API queries is https://api.bns.xyz
.
Create the API client
import { BnsApiClient } from "@bns-x/client";
const bns = new BnsApiClient();
// optionally, set base URL:
// new BnsApiClient('http://example.com');
Get the "display name" for an address
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);
Get details about a name
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
Fetch multiple names owned by an address
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
)
Interacting with BNS and BNSx contracts
This package includes clarigen generated types and functions for interacting with BNS contracts.
The BnsContractsClient
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');
Interacting with specific contracts
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
Usage with Clarigen
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>;
};
Examples of interacting with contracts:
BNS Core
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,
});
Transfer a BNSx name
contracts.registry.transfer({
id: 1,
sender: "SP123..",
recipient: "SP123..",
});
Unwrap a BNSx name
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",
});
Getting source code for a name wrapper contract
If you need to deploy a name wrapper contract, you can get the source code from nameWrapperCode
.
const code = contracts.nameWrapperCode();
Zonefiles
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.
Getting a BTC address
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;
}
Get an arbitrary TXT record
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`
Utility functions
This library exposes a few utility functions that come in handy when working with BNS.
asciiToBytes
and bytesToAscii
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;
randomSalt
When preordering a name on BNS, you need to create a random salt.
import { randomSalt } from "@bns-x/client";
const salt = randomSalt(); // Uint8Array
hashFqn
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);
parseFqn
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' }
doesNamespaceExpire
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
Punycode
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.
toUnicode
Converts a punycode string to unicode.
import { toUnicode } from "@bns-x/client";
toUnicode("xn--1ug66vku9r8p9h.btc"); // returns '🧔♂️.btc'
toPunycode
Convert a unicode string to punycode.
import { toPunycode } from "@bns-x/client";
toPunycode("🧔♂️.btc"); // returns 'xn--1ug66vku9r8p9h.btc'
Zero-width-join characters and modifiers
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
fullDisplayName
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🟥)'