UNPKG

@stacks/keychain

Version:

A package for managing Stacks keychains

279 lines (239 loc) 8.22 kB
import { Buffer } from '@stacks/common'; import { createSha2Hash, publicKeyToAddress } from '@stacks/encryption'; import { createFetchFn, FetchFn } from '@stacks/network'; import { BIP32Interface } from 'bitcoinjs-lib'; import { parseZoneFile } from 'zone-file'; import Identity from '../identity'; import IdentityAddressOwnerNode from '../nodes/identity-address-owner-node'; import { registrars, Subdomains } from '../profiles'; const IDENTITY_KEYCHAIN = 888; const BLOCKSTACK_ON_BITCOIN = 0; /** @deprecated */ export function getIdentityPrivateKeychain(rootNode: BIP32Interface) { return rootNode.deriveHardened(IDENTITY_KEYCHAIN).deriveHardened(BLOCKSTACK_ON_BITCOIN); } const EXTERNAL_ADDRESS = 'EXTERNAL_ADDRESS'; const CHANGE_ADDRESS = 'CHANGE_ADDRESS'; /** @deprecated */ export function getBitcoinPrivateKeychain(rootNode: BIP32Interface) { const BIP_44_PURPOSE = 44; const BITCOIN_COIN_TYPE = 0; const ACCOUNT_INDEX = 0; return rootNode .deriveHardened(BIP_44_PURPOSE) .deriveHardened(BITCOIN_COIN_TYPE) .deriveHardened(ACCOUNT_INDEX); } /** @deprecated */ export function getBitcoinAddressNode( bitcoinKeychain: BIP32Interface, addressIndex = 0, chainType = EXTERNAL_ADDRESS ) { let chain = null; if (chainType === EXTERNAL_ADDRESS) { chain = 0; } else if (chainType === CHANGE_ADDRESS) { chain = 1; } else { throw new Error('Invalid chain type'); } return bitcoinKeychain.derive(chain).derive(addressIndex); } /** @deprecated */ export async function getIdentityOwnerAddressNode( identityPrivateKeychain: BIP32Interface, identityIndex = 0 ) { if (identityPrivateKeychain.isNeutered()) { throw new Error('You need the private key to generate identity addresses'); } const publicKeyHex = Buffer.from(identityPrivateKeychain.publicKey.toString('hex')); const sha2Hash = await createSha2Hash(); const saltData = await sha2Hash.digest(publicKeyHex, 'sha256'); const salt = saltData.toString('hex'); return new IdentityAddressOwnerNode(identityPrivateKeychain.deriveHardened(identityIndex), salt); } export function getAddress(node: BIP32Interface) { return publicKeyToAddress(node.publicKey); } export interface IdentityKeyPair { key: string; keyID: string; address: string; appsNodeKey: string; stxNodeKey: string; salt: string; } /** @deprecated */ export function deriveIdentityKeyPair( identityOwnerAddressNode: IdentityAddressOwnerNode ): IdentityKeyPair { const address = identityOwnerAddressNode.getAddress(); const identityKey = identityOwnerAddressNode.getIdentityKey(); const identityKeyID = identityOwnerAddressNode.getIdentityKeyID(); const appsNode = identityOwnerAddressNode.getAppsNode(); const stxNode = identityOwnerAddressNode.getSTXNode(); const keyPair = { key: identityKey, keyID: identityKeyID, address, appsNodeKey: appsNode.toBase58(), stxNodeKey: stxNode.toBase58(), salt: identityOwnerAddressNode.getSalt(), }; return keyPair; } /** @deprecated */ export async function getBlockchainIdentities( rootNode: BIP32Interface, identitiesToGenerate: number ) { const identityPrivateKeychainNode = getIdentityPrivateKeychain(rootNode); const bitcoinPrivateKeychainNode = getBitcoinPrivateKeychain(rootNode); const identityPublicKeychainNode = identityPrivateKeychainNode.neutered(); const identityPublicKeychain = identityPublicKeychainNode.toBase58(); const bitcoinPublicKeychainNode = bitcoinPrivateKeychainNode.neutered(); const bitcoinPublicKeychain = bitcoinPublicKeychainNode.toBase58(); const firstBitcoinAddress = getAddress(getBitcoinAddressNode(bitcoinPublicKeychainNode)); const identityAddresses: string[] = []; const identityKeypairs = []; const identities: Identity[] = []; // We pre-generate a number of identity addresses so that we // don't have to prompt the user for the password on each new profile for (let addressIndex = 0; addressIndex < identitiesToGenerate; addressIndex++) { const identity = await makeIdentity(rootNode, addressIndex); identities.push(identity); identityKeypairs.push(identity.keyPair); identityAddresses.push(identity.address); } return { identityPublicKeychain, bitcoinPublicKeychain, firstBitcoinAddress, identityAddresses, identityKeypairs, identities, }; } /** @deprecated */ export const makeIdentity = async (rootNode: BIP32Interface, index: number) => { const identityPrivateKeychainNode = getIdentityPrivateKeychain(rootNode); const identityOwnerAddressNode = await getIdentityOwnerAddressNode( identityPrivateKeychainNode, index ); const identityKeyPair = deriveIdentityKeyPair(identityOwnerAddressNode); const identity = new Identity({ keyPair: identityKeyPair, address: identityKeyPair.address, usernames: [], }); return identity; }; export function assertIsTruthy<T>(val: any): asserts val is NonNullable<T> { if (!val) { throw new Error(`Assertion error: ${JSON.stringify({ expected: true, actual: val })}`); } } export enum IdentityNameValidityError { MINIMUM_LENGTH = 'error_minimum_length', MAXIMUM_LENGTH = 'error_maximum_length', ILLEGAL_CHARACTER = 'error_illegal_character', UNAVAILABLE = 'error_name_unavailable', } const containsLegalCharacters = (name: string) => /^[a-z0-9_]+$/.test(name); /** @deprecated */ export const validateSubdomainFormat = (identityName: string): IdentityNameValidityError | null => { const nameLength = identityName.length; if (nameLength < 8) { return IdentityNameValidityError.MINIMUM_LENGTH; } if (nameLength > 37) { return IdentityNameValidityError.MAXIMUM_LENGTH; } if (!containsLegalCharacters(identityName)) { return IdentityNameValidityError.ILLEGAL_CHARACTER; } return null; }; /** @deprecated */ export const validateSubdomainAvailability = async ( name: string, subdomain: Subdomains = Subdomains.BLOCKSTACK, fetchFn: FetchFn = createFetchFn() ) => { const url = `${registrars[subdomain].apiUrl}/${name.toLowerCase()}.${subdomain}`; const resp = await fetchFn(url); const data = await resp.json(); return data; }; interface RecursiveMakeIdentitiesOptions { rootNode: BIP32Interface; index?: number; identities?: Identity[]; } /** * Restore identities by recursively making a new identity, and checking if it has a username. * * As soon as a username is not found for an identity, the recursion stops. * @deprecated */ export const recursiveRestoreIdentities = async ({ rootNode, index = 1, identities = [], }: RecursiveMakeIdentitiesOptions): Promise<Identity[]> => { const identity = await makeIdentity(rootNode, index); await identity.refresh(); if (identity.defaultUsername) { identities.push(identity); return recursiveRestoreIdentities({ rootNode, index: index + 1, identities }); } return identities; }; /** * Validate the format and availability of a subdomain. Will return an error of enum * IdentityNameValidityError if an error is present. If no errors are found, will return null. * @param name the subdomain to be registered * @param subdomain a valid Subdomains enum */ export const validateSubdomain = async ( name: string, subdomain: Subdomains = Subdomains.BLOCKSTACK ) => { const error = validateSubdomainFormat(name); if (error) { return error; } try { const data = await validateSubdomainAvailability(name, subdomain); if (data.status !== 'available') { return IdentityNameValidityError.UNAVAILABLE; } } catch (error) { return IdentityNameValidityError.UNAVAILABLE; } return null; }; interface NameInfoResponse { address: string; zonefile: string; } export const getProfileURLFromZoneFile = async ( name: string, fetchFn: FetchFn = createFetchFn() ) => { const url = `https://stacks-node-api.stacks.co/v1/names/${name}`; const res = await fetchFn(url); if (res.ok) { const nameInfo: NameInfoResponse = await res.json(); const zone = parseZoneFile(nameInfo.zonefile); const uri = zone.uri?.[0]?.target; if (uri) { return uri; } throw new Error(`No zonefile uri found: ${nameInfo.zonefile}`); } return; };