libnexa-ts
Version:
A pure and powerful Nexa SDK library.
390 lines (342 loc) • 14.8 kB
text/typescript
import type { HDPublicKeyBuffers, HDPublicKeyDto, HDPublicKeyMinimalDto, IHDPrivateKey, IHDPublicKey } from "../common/interfaces";
import HDKeyUtils from "./hdkey.utils";
import { networks } from "../core/network/network-manager";
import BufferUtils from "../utils/buffer.utils";
import type { Network } from "../core/network/network";
import ValidationUtils from "../utils/validation.utils";
import { InvalidB58Checksum, InvalidDerivationArgument, InvalidHardenedIndex, InvalidPath } from "./exceptions";
import Base58Check from "../encoding/base58check";
import PublicKey from "./publickey";
import Hash from "../crypto/hash";
import BN from "../crypto/bn.extension";
import Point from "../crypto/point";
import { AddressType } from "../core/address/address-formatter";
import Address from "../core/address/address";
export default class HDPublicKey implements IHDPublicKey {
private static readonly PublicKeySize = 33;
private static readonly PublicKeyStart = HDKeyUtils.ChainCodeEnd;
private static readonly PublicKeyEnd = this.PublicKeyStart + this.PublicKeySize;
private static readonly ChecksumStart = this.PublicKeyEnd;
private static readonly ChecksumEnd = this.ChecksumStart + HDKeyUtils.CheckSumSize;
readonly publicKey!: PublicKey;
readonly network!: Network;
readonly depth!: number;
readonly parentFingerPrint!: Uint8Array;
readonly fingerPrint!: Uint8Array;
readonly chainCode!: Uint8Array;
readonly childIndex!: number;
readonly checksum!: Uint8Array;
readonly xpubkey!: string;
/**
* The representation of an hierarchically derived public key.
*
* See https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
*
* @param arg
*/
constructor(arg: string | Uint8Array | HDPublicKeyDto | HDPublicKeyMinimalDto | IHDPublicKey | IHDPrivateKey) {
if (arg == null) {
throw new TypeError("Must supply an argument to create a HDPublicKey");
}
if (arg instanceof HDPublicKey) {
return arg;
}
let params = HDPublicKey._classifyArgument(arg);
this.publicKey = params.publicKey as PublicKey;
this.network = params.network;
this.depth = params.depth;
this.parentFingerPrint = params.parentFingerPrint;
this.fingerPrint = params.fingerPrint;
this.chainCode = params.chainCode;
this.childIndex = params.childIndex;
this.xpubkey = params.xpubkey;
this.checksum = params.checksum;
}
private static _classifyArgument(arg: string | Uint8Array | HDPublicKeyDto | HDPublicKeyMinimalDto | IHDPublicKey | IHDPrivateKey): IHDPublicKey {
if (typeof arg === 'string' || BufferUtils.isBuffer(arg)) {
let xpubkey = typeof arg === 'string' ? arg : BufferUtils.bufferToUtf8(arg);
if (HDKeyUtils.isValidSerialized(xpubkey)) {
return this._buildFromSerialized(xpubkey);
}
throw HDKeyUtils.getSerializedError(arg);
}
if (this._isIHDPubKey(arg)) {
return arg;
}
if (this._isMinimalDtoObject(arg)) {
return this._buildFromMinimalObject(arg);
}
if (this._isDtoObject(arg)) {
return this._buildFromObject(arg);
}
if (this._isIHDPrivKey(arg)) {
return this._buildFromHDPrivateKey(arg);
}
throw new TypeError('Invalid argument type for creation, must be string, json, buffer, or object');
}
private static _isDtoObject(data: unknown): data is HDPublicKeyDto {
return typeof data === 'object' && data !== null && 'xpubkey' in data && !('privateKey' in data) && 'publicKey' in data && typeof data.publicKey === 'string';
}
private static _isMinimalDtoObject(data: unknown): data is HDPublicKeyMinimalDto {
return typeof data === 'object' && data !== null && !('xpubkey' in data) && !('privateKey' in data) && 'publicKey' in data && typeof data.publicKey === 'string';
}
private static _isIHDPubKey(data: unknown): data is IHDPublicKey {
return typeof data === 'object' && data !== null && 'xpubkey' in data && !('privateKey' in data) && 'publicKey' in data && data.publicKey instanceof PublicKey;
}
private static _isIHDPrivKey(data: unknown): data is IHDPrivateKey {
return typeof data === 'object' && data !== null && 'privateKey' in data && typeof data.privateKey === 'object' && data.privateKey !== null && 'bn' in data.privateKey;
}
/**
* Verifies that a given path is valid.
*
* @param arg
* @return {boolean}
*/
public static isValidPath(arg: string | number): boolean {
if (typeof arg === 'string') {
let indexes = HDKeyUtils.getDerivationIndexes(arg);
return indexes !== null && indexes.every(HDPublicKey.isValidPath);
}
if (typeof arg === 'number') {
return arg >= 0 && arg < HDKeyUtils.Hardened;
}
return false;
}
/**
* Create a HDPublicKey from a buffer argument
*
* @param buf
*/
public static fromBuffer(buf: Uint8Array): HDPublicKey {
ValidationUtils.validateArgument(BufferUtils.isBuffer(buf), 'No valid Buffer was provided');
return new HDPublicKey(buf);
}
/**
* Return a buffer representation of the xpubkey
*/
public toBuffer(): Uint8Array {
return BufferUtils.utf8ToBuffer(this.xpubkey);
}
public static fromString(xpubkey: string): HDPublicKey {
ValidationUtils.validateArgument(typeof xpubkey === 'string', 'No valid string was provided');
return new HDPublicKey(xpubkey);
}
/**
* Returns the base58 checked representation of the public key
* @return a string starting with "xpub..." in livenet
*/
public toString(): string {
return this.xpubkey;
}
/**
* Returns the console representation of this extended public key.
*/
public inspect(): string {
return `<HDPublicKey: ${this}>`;
}
public static fromObject(arg: HDPublicKeyDto): HDPublicKey {
ValidationUtils.validateArgument(typeof arg === 'object' && arg !== null, 'No valid argument was provided');
return new HDPublicKey(arg);
}
public static fromMinimalObject(arg: HDPublicKeyMinimalDto): HDPublicKey {
ValidationUtils.validateArgument(typeof arg === 'object' && arg !== null, 'No valid argument was provided');
let params = this._buildFromMinimalObject(arg);
return new HDPublicKey(params);
}
public toJSON = this.toObject;
/**
* Returns a plain JavaScript object with information to reconstruct a key.
*/
public toObject(): HDPublicKeyDto {
return {
network: this.network.name,
depth: this.depth,
fingerPrint: BufferUtils.integerFromBuffer(this.fingerPrint),
parentFingerPrint: BufferUtils.integerFromBuffer(this.parentFingerPrint),
childIndex: this.childIndex,
chainCode: BufferUtils.bufferToHex(this.chainCode),
publicKey: this.publicKey.toString(),
checksum: BufferUtils.integerFromBuffer(this.checksum),
xpubkey: this.xpubkey
};
}
/**
* Will return an address for the hdpubkey with its defined network
*
* @param type - optional parameter specifying the desired type of the address.
* default {@link AddressType.PayToScriptTemplate}
*
* @returns An address generated from the hd public key
*/
public toAddress(type = AddressType.PayToScriptTemplate): Address {
return Address.fromPublicKey(this.publicKey, this.network, type);
}
/**
* Get a derivated child based on a string or number.
*
* If the first argument is a string, it's parsed as the full path of
* derivation. Valid values for this argument include "m" (which returns the
* same public key), "m/0/1/40/2/1000".
*
* Note that hardened keys can't be derived from a public extended key.
*
* If the first argument is a number, the child with that index will be
* derived. See the example usage for clarification.
*
* @example
* ```javascript
* let parent = new HDPublicKey('xpub...');
* let child_0_1_2 = parent.deriveChild(0).deriveChild(1).deriveChild(2);
* let copy_of_child_0_1_2 = parent.deriveChild("m/0/1/2");
* assert(child_0_1_2.xpubkey === copy_of_child_0_1_2.xpubkey);
* ```
*
* @param {string|number} arg
*/
public deriveChild(arg: string | number, hardened?: boolean): HDPublicKey {
if (typeof arg === 'number') {
return this._deriveWithNumber(arg, hardened);
}
if (typeof arg === 'string') {
return this._deriveFromString(arg);
}
throw new InvalidDerivationArgument(arg);
}
private _deriveWithNumber(index: number, hardened?: boolean): HDPublicKey {
if (index >= HDKeyUtils.Hardened || hardened) {
throw new InvalidHardenedIndex();
}
if (index < 0) {
throw new InvalidPath(index);
}
let indexBuffer = BufferUtils.integerAsBuffer(index);
let data = BufferUtils.concat([this.publicKey.toBuffer(), indexBuffer]);
let hash = Hash.sha512hmac(data, this.chainCode);
let leftPart = BN.fromBuffer(hash.subarray(0, 32), {size: 32});
let chainCode = hash.subarray(32, 64);
let publicKey: PublicKey;
try {
publicKey = PublicKey.fromPoint(Point.getG().mul(leftPart).add(this.publicKey.point));
} catch {
return this._deriveWithNumber(index + 1);
}
let buffers: HDPublicKeyBuffers = {
version: BufferUtils.integerAsBuffer(this.network.xpubkey),
depth: BufferUtils.integerAsSingleByteBuffer(this.depth + 1),
parentFingerPrint: this.fingerPrint,
childIndex: BufferUtils.integerAsBuffer(index),
publicKey: publicKey.toBuffer(),
chainCode: chainCode
};
let derived = HDPublicKey._buildFromBuffers(buffers);
return new HDPublicKey(derived);
}
private _deriveFromString(path: string): HDPublicKey {
if (path.includes("'")) {
throw new InvalidHardenedIndex();
}
if (!HDPublicKey.isValidPath(path)) {
throw new InvalidPath(path);
}
const deriveKeys = (baseKey: HDPublicKey, indexes: number[]): HDPublicKey => {
return indexes.reduce((prev, idx) => prev._deriveWithNumber(idx), baseKey);
}
let indexes = HDKeyUtils.getDerivationIndexes(path);
return deriveKeys(this, indexes!);
}
private static _buildFromObject(arg: HDPublicKeyDto): IHDPublicKey {
return {
network: networks.get(arg.network)!,
depth: arg.depth,
fingerPrint: BufferUtils.integerAsBuffer(arg.fingerPrint),
parentFingerPrint: BufferUtils.integerAsBuffer(arg.parentFingerPrint),
childIndex: arg.childIndex,
chainCode: BufferUtils.hexToBuffer(arg.chainCode),
xpubkey: arg.xpubkey,
checksum: BufferUtils.integerAsBuffer(arg.checksum),
publicKey: PublicKey.fromString(arg.publicKey, networks.get(arg.network)!)
}
}
private static _buildFromMinimalObject(arg: HDPublicKeyMinimalDto): IHDPublicKey {
let buffers = {
version: BufferUtils.integerAsBuffer(networks.get(arg.network)!.xpubkey),
depth: BufferUtils.integerAsSingleByteBuffer(arg.depth),
parentFingerPrint: BufferUtils.integerAsBuffer(arg.parentFingerPrint),
childIndex: BufferUtils.integerAsBuffer(arg.childIndex),
chainCode: BufferUtils.hexToBuffer(arg.chainCode),
publicKey: BufferUtils.hexToBuffer(arg.publicKey),
};
return this._buildFromBuffers(buffers);
}
private static _buildFromHDPrivateKey(hdPrivateKey: IHDPrivateKey): IHDPublicKey {
let point = Point.getG().mul(hdPrivateKey.privateKey.bn);
let buffers: HDPublicKeyBuffers = {
version: BufferUtils.integerAsBuffer(hdPrivateKey.network.xpubkey),
depth: BufferUtils.integerAsSingleByteBuffer(hdPrivateKey.depth),
parentFingerPrint: hdPrivateKey.parentFingerPrint,
childIndex: BufferUtils.integerAsBuffer(hdPrivateKey.childIndex),
chainCode: hdPrivateKey.chainCode,
publicKey: Point.pointToCompressed(point),
};
return this._buildFromBuffers(buffers);
}
private static _buildFromSerialized(xpubkey: string): IHDPublicKey {
let decoded = Base58Check.decode(xpubkey);
let buffers: HDPublicKeyBuffers = {
version: decoded.subarray(HDKeyUtils.VersionStart, HDKeyUtils.VersionEnd),
depth: decoded.subarray(HDKeyUtils.DepthStart, HDKeyUtils.DepthEnd),
parentFingerPrint: decoded.subarray(HDKeyUtils.ParentFingerPrintStart, HDKeyUtils.ParentFingerPrintEnd),
childIndex: decoded.subarray(HDKeyUtils.ChildIndexStart, HDKeyUtils.ChildIndexEnd),
chainCode: decoded.subarray(HDKeyUtils.ChainCodeStart, HDKeyUtils.ChainCodeEnd),
publicKey: decoded.subarray(this.PublicKeyStart, this.PublicKeyEnd),
checksum: decoded.subarray(this.ChecksumStart, this.ChecksumEnd),
};
return this._buildFromBuffers(buffers);
}
/**
* Receives a object with buffers in all the properties and populates the
* internal structure
*
* @param arg
*/
private static _buildFromBuffers(arg: HDPublicKeyBuffers): IHDPublicKey {
HDPublicKey._validateBufferArguments(arg);
let sequence = [ arg.version, arg.depth, arg.parentFingerPrint, arg.childIndex, arg.chainCode, arg.publicKey ];
let concat = BufferUtils.concat(sequence);
let checksum = Base58Check.checksum(concat);
if (!arg.checksum || !arg.checksum.length) {
arg.checksum = checksum;
} else if (BufferUtils.bufferToHex(arg.checksum) !== BufferUtils.bufferToHex(checksum)) {
throw new InvalidB58Checksum(concat);
}
let network = networks.get(BufferUtils.integerFromBuffer(arg.version))!;
let publicKey = PublicKey.fromBuffer(arg.publicKey, true, network);
return {
xpubkey: Base58Check.encode(concat),
network: network,
depth: BufferUtils.integerFromSingleByteBuffer(arg.depth),
publicKey: publicKey,
fingerPrint: Hash.sha256ripemd160(publicKey.toBuffer()).subarray(0, HDKeyUtils.ParentFingerPrintSize),
chainCode: arg.chainCode,
childIndex: BufferUtils.integerFromBuffer(arg.childIndex),
parentFingerPrint: arg.parentFingerPrint,
checksum: arg.checksum
};
}
private static _validateBufferArguments(arg: HDPublicKeyBuffers): void {
const checkBuffer = (name: string, size: number): void => {
let buff = arg[name as keyof HDPublicKeyBuffers];
ValidationUtils.validateArgument(BufferUtils.isBuffer(buff), name + ` argument is not a buffer, it\'s ${typeof buff}`);
ValidationUtils.validateArgument(buff!.length === size, name + ' has not the expected size: found ' + buff!.length + ', expected ' + size);
};
checkBuffer('version', HDKeyUtils.VersionSize);
checkBuffer('depth', HDKeyUtils.DepthSize);
checkBuffer('parentFingerPrint', HDKeyUtils.ParentFingerPrintSize);
checkBuffer('childIndex', HDKeyUtils.ChildIndexSize);
checkBuffer('chainCode', HDKeyUtils.ChainCodeSize);
checkBuffer('publicKey', HDPublicKey.PublicKeySize);
if (arg.checksum?.length) {
checkBuffer('checksum', HDKeyUtils.CheckSumSize);
}
}
}