@stacks/keychain
Version:
A package for managing Stacks keychains
279 lines (239 loc) • 8.22 kB
text/typescript
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;
};