UNPKG

@stacks/bns

Version:

Library for working with the Stacks Blockchain Naming System BNS.

820 lines (765 loc) 24.4 kB
import { IntegerType, PublicKey, intToBigInt, utf8ToBytes } from '@stacks/common'; import { NetworkClientParam, StacksNetwork } from '@stacks/network'; import { ClarityType, ClarityValue, NonFungiblePostCondition, PostCondition, ResponseErrorCV, StacksTransactionWire, StxPostCondition, UnsignedContractCallOptions, bufferCV, bufferCVFromString, cvToString, fetchCallReadOnlyFunction, getAddressFromPrivateKey, getCVTypeString, hash160, makeRandomPrivKey, makeUnsignedContractCall, noneCV, publicKeyToAddress, someCV, standardPrincipalCV, tupleCV, uintCV, } from '@stacks/transactions'; import { decodeFQN, getZonefileHash } from './utils'; export const BNS_CONTRACT_NAME = 'bns'; export interface PriceFunction { base: IntegerType; coefficient: IntegerType; b1: IntegerType; b2: IntegerType; b3: IntegerType; b4: IntegerType; b5: IntegerType; b6: IntegerType; b7: IntegerType; b8: IntegerType; b9: IntegerType; b10: IntegerType; b11: IntegerType; b12: IntegerType; b13: IntegerType; b14: IntegerType; b15: IntegerType; b16: IntegerType; nonAlphaDiscount: IntegerType; noVowelDiscount: IntegerType; } export interface BnsContractCallOptions { functionName: string; functionArgs: ClarityValue[]; publicKey: PublicKey; network: StacksNetwork; postConditions?: PostCondition[]; } async function makeBnsContractCall( options: BnsContractCallOptions ): Promise<StacksTransactionWire> { const txOptions: UnsignedContractCallOptions = { contractAddress: options.network.bootAddress, contractName: BNS_CONTRACT_NAME, functionName: options.functionName, functionArgs: options.functionArgs, publicKey: options.publicKey, validateWithAbi: false, network: options.network, postConditions: options.postConditions, }; return makeUnsignedContractCall(txOptions); } export interface BnsReadOnlyOptions { functionName: string; functionArgs: ClarityValue[]; senderAddress: string; network: StacksNetwork; } async function callReadOnlyBnsFunction( options: BnsReadOnlyOptions & NetworkClientParam ): Promise<ClarityValue> { return fetchCallReadOnlyFunction({ ...options, contractAddress: options.network.bootAddress, contractName: BNS_CONTRACT_NAME, }); } /** * Can register name options * * @param {String} fullyQualifiedName - the fully qualified name ("name.namespace") to check * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface CanRegisterNameOptions { fullyQualifiedName: string; network: StacksNetwork; } /** * Check if name can be registered * * @param {string} fullyQualifiedName - the fully qualified name to check * @param {StacksNetwork} network - the Stacks network to broadcast transaction to * * @returns {Promise} that resolves to true if the operation succeeds */ export async function canRegisterName({ fullyQualifiedName, network, }: CanRegisterNameOptions): Promise<boolean> { const bnsFunctionName = 'can-name-be-registered'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot register a subdomain using registerName'); } // Create a random address as input to read-only function call // Not used by BNS contract function but required by core node API // https://github.com/blockstack/stacks-blockchain/blob/master/src/net/http.rs#L1796 const randomPrivateKey = makeRandomPrivKey(); const randomAddress = getAddressFromPrivateKey(randomPrivateKey); return callReadOnlyBnsFunction({ functionName: bnsFunctionName, senderAddress: randomAddress, functionArgs: [bufferCVFromString(namespace), bufferCVFromString(name)], network, }).then((responseCV: ClarityValue) => { if (responseCV.type === ClarityType.ResponseOk) { return responseCV.value.type === ClarityType.BoolTrue; } else { return false; } }); } /** * Get namespace price options * * @param {String} namespace - the namespace to get the price of * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface GetNamespacePriceOptions { namespace: string; network: StacksNetwork; } /** * Get price of namespace registration in microstacks * * @param {string} namespace - the namespace * @param {StacksNetwork} network - the Stacks network to use * * @returns {Promise} that resolves to a BN object number of microstacks if the operation succeeds */ export async function getNamespacePrice({ namespace, network, }: GetNamespacePriceOptions): Promise<bigint> { const bnsFunctionName = 'get-namespace-price'; // Create a random address as input to read-only function call // Not used by BNS contract function but required by core node API // https://github.com/blockstack/stacks-blockchain/blob/master/src/net/http.rs#L1796 const randomPrivateKey = makeRandomPrivKey(); const randomAddress = getAddressFromPrivateKey(randomPrivateKey); return callReadOnlyBnsFunction({ functionName: bnsFunctionName, senderAddress: randomAddress, functionArgs: [bufferCVFromString(namespace)], network, }).then((responseCV: ClarityValue) => { if (responseCV.type === ClarityType.ResponseOk) { if (responseCV.value.type === ClarityType.Int || responseCV.value.type === ClarityType.UInt) { return BigInt(responseCV.value.value); } else { throw new Error('Response did not contain a number'); } } else if (responseCV.type === ClarityType.ResponseErr) { throw new Error(cvToString(responseCV.value)); } else { throw new Error(`Unexpected Clarity Value type: ${getCVTypeString(responseCV)}`); } }); } /** * Get name price options * * @param {String} fullyQualifiedName - the fully qualified name ("name.namespace") to get the price of * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface GetNamePriceOptions { fullyQualifiedName: string; network: StacksNetwork; } /** * Get price of name registration in microstacks * * @param {string} fullyQualifiedName - the fully qualified name * @param {StacksNetwork} network - the Stacks network to use * * @returns {Promise} that resolves to a BN object number of microstacks if the operation succeeds */ export async function getNamePrice({ fullyQualifiedName, network, }: GetNamePriceOptions): Promise<bigint> { const bnsFunctionName = 'get-name-price'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot get subdomain name price'); } // Create a random address as input to read-only function call // Not used by BNS contract function but required by core node API // https://github.com/blockstack/stacks-blockchain/blob/master/src/net/http.rs#L1796 const randomPrivateKey = makeRandomPrivKey(); const randomAddress = getAddressFromPrivateKey(randomPrivateKey); return callReadOnlyBnsFunction({ functionName: bnsFunctionName, senderAddress: randomAddress, functionArgs: [bufferCVFromString(namespace), bufferCVFromString(name)], network, }).then((responseCV: ClarityValue) => { if (responseCV.type === ClarityType.ResponseOk) { if (responseCV.value.type === ClarityType.Int || responseCV.value.type === ClarityType.UInt) { return BigInt(responseCV.value.value); } else { throw new Error('Response did not contain a number'); } } else { const errorResponse = responseCV as ResponseErrorCV; throw new Error(cvToString(errorResponse.value)); } }); } /** * Preorder namespace options */ export interface PreorderNamespaceOptions { /** the namespace to preorder */ namespace: string; /** salt used to generate the preorder namespace hash */ salt: string; /** amount of STX to burn for the registration */ stxToBurn: IntegerType; /** the private key to sign the transaction */ publicKey: string; /** the Stacks blockchain network to use */ network: StacksNetwork; } /** * Generates a namespace preorder transaction. * First step in registering a namespace. This transaction does not reveal the namespace that is * about to be registered. And it sets the amount of STX to be burned for the registration. * * Resolves to the generated StacksTransaction * * @param {PreorderNamespaceOptions} options - an options object for the preorder * * @return {Promise<StacksTransactionWire>} */ export async function buildPreorderNamespaceTx({ namespace, salt, stxToBurn, publicKey, network, }: PreorderNamespaceOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'namespace-preorder'; const saltedNamespaceBytes = utf8ToBytes(`${namespace}${salt}`); const hashedSaltedNamespace = hash160(saltedNamespaceBytes); const burnSTXPostCondition: StxPostCondition = { type: 'stx-postcondition', address: publicKeyToAddress(network.addressVersion.singleSig, publicKey), condition: 'eq', amount: intToBigInt(stxToBurn), }; return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [bufferCV(hashedSaltedNamespace), uintCV(stxToBurn)], publicKey, network, postConditions: [burnSTXPostCondition], }); } /** * Reveal namespace options */ export interface RevealNamespaceOptions { /** the namespace to reveal */ namespace: string; /** salt used to generate the preorder namespace hash */ salt: string; /** an object containing the price function for the namespace */ priceFunction: PriceFunction; /** the number of blocks name registrations are valid for in the namespace */ lifetime: IntegerType; /** the STX address used for name imports */ namespaceImportAddress: string; /** the key to sign the transaction */ publicKey: string; /** the Stacks blockchain network to use */ network: StacksNetwork; } /** * Generates a namespace reveal transaction. * Second step in registering a namespace. * * Resolves to the generated StacksTransaction * * @param {RevealNamespaceOptions} options - an options object for the reveal * * @return {Promise<StacksTransactionWire>} */ export async function buildRevealNamespaceTx({ namespace, salt, priceFunction, lifetime, namespaceImportAddress, publicKey, network, }: RevealNamespaceOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'namespace-reveal'; return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [ bufferCVFromString(namespace), bufferCVFromString(salt), uintCV(priceFunction.base), uintCV(priceFunction.coefficient), uintCV(priceFunction.b1), uintCV(priceFunction.b2), uintCV(priceFunction.b3), uintCV(priceFunction.b4), uintCV(priceFunction.b5), uintCV(priceFunction.b6), uintCV(priceFunction.b7), uintCV(priceFunction.b8), uintCV(priceFunction.b9), uintCV(priceFunction.b10), uintCV(priceFunction.b11), uintCV(priceFunction.b12), uintCV(priceFunction.b13), uintCV(priceFunction.b14), uintCV(priceFunction.b15), uintCV(priceFunction.b16), uintCV(priceFunction.nonAlphaDiscount), uintCV(priceFunction.noVowelDiscount), uintCV(lifetime), standardPrincipalCV(namespaceImportAddress), ], publicKey, network, }); } /** * Namespace name import options * * @param {String} namespace - the namespace to import name into * @param {String} name - the name to import * @param {String} beneficiary - the address to register the name to * @param {String} zonefileHash - the zonefile hash to register * @param {String} publicKey - the private key to sign the transaction * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface ImportNameOptions { namespace: string; name: string; beneficiary: string; zonefile: string; publicKey: string; network: StacksNetwork; } /** * Generates a namespace name import transaction. * An optional step in namespace registration. * * Resolves to the generated StacksTransaction * * @param {ImportNameOptions} options - an options object for the name import * * @return {Promise<StacksTransactionWire>} */ export async function buildImportNameTx({ namespace, name, beneficiary, zonefile, publicKey, network, }: ImportNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-import'; const zonefileHash = getZonefileHash(zonefile); return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [ bufferCVFromString(namespace), bufferCVFromString(name), standardPrincipalCV(beneficiary), bufferCV(zonefileHash), ], publicKey, network, }); } /** * Ready namespace options * * @param {String} namespace - the namespace to ready * @param {String} publicKey - the private key to sign the transaction * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface ReadyNamespaceOptions { namespace: string; publicKey: string; network: StacksNetwork; } /** * Generates a ready namespace transaction. * Final step in namespace registration. This completes the namespace registration and * makes the namespace available for name registrations. * * Resolves to the generated StacksTransaction * * @param {ReadyNamespaceOptions} options - an options object for the namespace ready transaction * * @return {Promise<StacksTransactionWire>} */ export async function buildReadyNamespaceTx({ namespace, publicKey, network, }: ReadyNamespaceOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'namespace-ready'; return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [bufferCVFromString(namespace)], publicKey, network, }); } /** * Preorder name options */ export interface PreorderNameOptions { /** the fully qualified name to preorder including the namespace (myName.id) */ fullyQualifiedName: string; /** salt used to generate the preorder name hash */ salt: string; /** amount of STX to burn for the registration */ stxToBurn: IntegerType; /** the private key to sign the transaction */ publicKey: PublicKey; /** the Stacks blockchain network to use */ network: StacksNetwork; } /** * Generates a name preorder transaction. * First step in registering a name. This transaction does not reveal the name that is * about to be registered. And it sets the amount of STX to be burned for the registration. * * Resolves to the generated StacksTransaction * * @param {PreorderNameOptions} options - an options object for the preorder * * @return {Promise<StacksTransactionWire>} */ export async function buildPreorderNameTx({ fullyQualifiedName, salt, stxToBurn, publicKey, network, }: PreorderNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-preorder'; const { subdomain } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot preorder a subdomain using preorderName()'); } const saltedNamesBytes = utf8ToBytes(`${fullyQualifiedName}${salt}`); const hashedSaltedName = hash160(saltedNamesBytes); const burnSTXPostCondition: StxPostCondition = { type: 'stx-postcondition', address: publicKeyToAddress(network.addressVersion.singleSig, publicKey), condition: 'eq', amount: intToBigInt(stxToBurn), }; return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [bufferCV(hashedSaltedName), uintCV(stxToBurn)], publicKey, network, postConditions: [burnSTXPostCondition], }); } /** * Register name options * * @param {String} fullyQualifiedName - the fully qualified name to preorder including the * namespace (myName.id) * @param {String} salt - salt used to generate the preorder name hash * @param {String} zonefile - the zonefile to register with the name * @param {String} publicKey - the private key to sign the transaction * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface RegisterNameOptions { fullyQualifiedName: string; salt: string; zonefile: string; publicKey: PublicKey; network: StacksNetwork; } /** * Generates a name registration transaction. * Second and final step in registering a name. * * Resolves to the generated StacksTransaction * * @param {RegisterNameOptions} options - an options object for the registration * * @return {Promise<StacksTransactionWire>} */ export async function buildRegisterNameTx({ fullyQualifiedName, salt, zonefile, publicKey, network, }: RegisterNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-register'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot register a subdomain using registerName()'); } const zonefileHash = getZonefileHash(zonefile); return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [ bufferCVFromString(namespace), bufferCVFromString(name), bufferCVFromString(salt), bufferCV(zonefileHash), ], network, publicKey, }); } /** * Update name options * * @param {String} fullyQualifiedName - the fully qualified name to update including the * namespace (myName.id) * @param {String} zonefile - the zonefile to register with the name * @param {String} publicKey - the private key to sign the transaction * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface UpdateNameOptions { fullyQualifiedName: string; zonefile: string; publicKey: string; network: StacksNetwork; } /** * Generates a name update transaction. * This changes the zonefile for the registered name. * * Resolves to the generated StacksTransaction * * @param {UpdateNameOptions} options - an options object for the update * * @return {Promise<StacksTransactionWire>} */ export async function buildUpdateNameTx({ fullyQualifiedName, zonefile, publicKey, network, }: UpdateNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-update'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot update a subdomain using updateName()'); } const zonefileHash = getZonefileHash(zonefile); return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [bufferCVFromString(namespace), bufferCVFromString(name), bufferCV(zonefileHash)], publicKey, network, }); } /** * Transfer name options * * @param {String} fullyQualifiedName - the fully qualified name to transfer including the * namespace (myName.id) * @param {String} newOwnerAddress - the recipient address of the name transfer * @param {String} zonefile - the optional zonefile to register with the name * @param {String} publicKey - the private key to sign the transaction * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface TransferNameOptions { fullyQualifiedName: string; newOwnerAddress: string; publicKey: string; network: StacksNetwork; zonefile?: string; } /** * Generates a name transfer transaction. * This changes the owner of the registered name. * * Since the underlying NFT will be transferred, * you will be required to add a post-condition to this * transaction before broadcasting it. * * Resolves to the generated StacksTransaction * * @param {TransferNameOptions} options - an options object for the transfer * * @return {Promise<StacksTransactionWire>} */ export async function buildTransferNameTx({ fullyQualifiedName, newOwnerAddress, zonefile, publicKey, network, }: TransferNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-transfer'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot transfer a subdomain using transferName()'); } const functionArgs = [ bufferCVFromString(namespace), bufferCVFromString(name), standardPrincipalCV(newOwnerAddress), zonefile ? someCV(bufferCV(getZonefileHash(zonefile))) : noneCV(), ]; const postConditionSender: NonFungiblePostCondition = { type: 'nft-postcondition', address: publicKeyToAddress(network.addressVersion.singleSig, publicKey), condition: 'sent', asset: `${network.bootAddress}.bns::names`, assetId: tupleCV({ name: bufferCVFromString(name), namespace: bufferCVFromString(namespace), }), }; const postConditionReceiver: NonFungiblePostCondition = { type: 'nft-postcondition', address: newOwnerAddress, condition: 'not-sent', asset: `${network.bootAddress}.bns::names`, assetId: tupleCV({ name: bufferCVFromString(name), namespace: bufferCVFromString(namespace), }), }; return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs, publicKey, network, postConditions: [postConditionSender, postConditionReceiver], }); } /** * Revoke name options * * @param {String} fullyQualifiedName - the fully qualified name to revoke including the * namespace (myName.id) * @param {String} publicKey - the private key to sign the transaction * @param {StacksNetwork} network - the Stacks blockchain network to use */ export interface RevokeNameOptions { fullyQualifiedName: string; publicKey: string; network: StacksNetwork; } /** * Generates a name revoke transaction. * This revokes a name registration. * * Resolves to the generated StacksTransaction * * @param {RevokeNameOptions} options - an options object for the revoke * * @return {Promise<StacksTransactionWire>} */ export async function buildRevokeNameTx({ fullyQualifiedName, publicKey, network, }: RevokeNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-revoke'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot revoke a subdomain using revokeName()'); } return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs: [bufferCVFromString(namespace), bufferCVFromString(name)], publicKey, network, }); } /** * Renew name options */ export interface RenewNameOptions { /** the fully qualified name to renew including the namespace (myName.id) */ fullyQualifiedName: string; /** amount of STX to burn for the registration */ stxToBurn: IntegerType; /** the private key to sign the transaction */ publicKey: string; /** the Stacks blockchain network to use */ network: StacksNetwork; /** optionally choose a new owner address */ newOwnerAddress?: string; /** optionally update the zonefile hash */ zonefile?: string; } /** * Generates a name renew transaction. * This renews a name registration. * * Resolves to the generated StacksTransaction * * @param {RenewNameOptions} options - an options object for the renew * * @return {Promise<StacksTransactionWire>} */ export async function buildRenewNameTx({ fullyQualifiedName, stxToBurn, newOwnerAddress, zonefile, publicKey, network, }: RenewNameOptions): Promise<StacksTransactionWire> { const bnsFunctionName = 'name-renewal'; const { subdomain, namespace, name } = decodeFQN(fullyQualifiedName); if (subdomain) { throw new Error('Cannot renew a subdomain using renewName()'); } const functionArgs = [ bufferCVFromString(namespace), bufferCVFromString(name), uintCV(stxToBurn), newOwnerAddress ? someCV(standardPrincipalCV(newOwnerAddress)) : noneCV(), zonefile ? someCV(bufferCV(getZonefileHash(zonefile))) : noneCV(), ]; const burnSTXPostCondition: StxPostCondition = { type: 'stx-postcondition', address: publicKeyToAddress(network.addressVersion.singleSig, publicKey), condition: 'eq', amount: intToBigInt(stxToBurn), }; return makeBnsContractCall({ functionName: bnsFunctionName, functionArgs, publicKey, network, postConditions: [burnSTXPostCondition], }); }