lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
417 lines (416 loc) • 15.9 kB
JavaScript
import { BN } from './crypto/bn.js';
import { PublicKey } from './publickey.js';
import { HDPrivateKey } from './hdprivatekey.js';
import { get as getNetwork } from './networks.js';
import { Hash } from './crypto/hash.js';
import { Base58Check } from './encoding/base58check.js';
import { JSUtil } from './util/js.js';
import { Preconditions } from './util/preconditions.js';
import { Point } from './crypto/point.js';
export class HDPublicKey {
xpubkey;
network;
depth;
publicKey;
fingerPrint;
parentFingerPrint;
childIndex;
chainCode;
_buffers;
static Hardened = 0x80000000;
static RootElementAlias = ['m', 'M'];
static VersionSize = 4;
static DepthSize = 1;
static ParentFingerPrintSize = 4;
static ChildIndexSize = 4;
static ChainCodeSize = 32;
static PublicKeySize = 33;
static CheckSumSize = 4;
static DataSize = 78;
static SerializedByteSize = 82;
static VersionStart = 0;
static VersionEnd = HDPublicKey.VersionStart + HDPublicKey.VersionSize;
static DepthStart = HDPublicKey.VersionEnd;
static DepthEnd = HDPublicKey.DepthStart + HDPublicKey.DepthSize;
static ParentFingerPrintStart = HDPublicKey.DepthEnd;
static ParentFingerPrintEnd = HDPublicKey.ParentFingerPrintStart + HDPublicKey.ParentFingerPrintSize;
static ChildIndexStart = HDPublicKey.ParentFingerPrintEnd;
static ChildIndexEnd = HDPublicKey.ChildIndexStart + HDPublicKey.ChildIndexSize;
static ChainCodeStart = HDPublicKey.ChildIndexEnd;
static ChainCodeEnd = HDPublicKey.ChainCodeStart + HDPublicKey.ChainCodeSize;
static PublicKeyStart = HDPublicKey.ChainCodeEnd;
static PublicKeyEnd = HDPublicKey.PublicKeyStart + HDPublicKey.PublicKeySize;
static ChecksumStart = HDPublicKey.PublicKeyEnd;
static ChecksumEnd = HDPublicKey.ChecksumStart + HDPublicKey.CheckSumSize;
constructor(arg) {
if (arg instanceof HDPublicKey) {
return arg;
}
if (!(this instanceof HDPublicKey)) {
return new HDPublicKey(arg);
}
if (arg) {
if (typeof arg === 'string' || Buffer.isBuffer(arg)) {
const error = HDPublicKey.getSerializedError(arg);
if (!error) {
return this._buildFromSerialized(arg);
}
else if (Buffer.isBuffer(arg) &&
!HDPublicKey.getSerializedError(arg.toString())) {
return this._buildFromSerialized(arg.toString());
}
else {
throw error;
}
}
else {
if (typeof arg === 'object' && arg !== null) {
if (arg instanceof HDPrivateKey) {
return this._buildFromPrivate(arg);
}
else {
return this._buildFromObject(arg);
}
}
else {
throw new Error('Unrecognized argument');
}
}
}
else {
throw new Error('Must supply an argument to create a HDPublicKey');
}
}
static isValidPath(arg) {
if (typeof arg === 'string') {
const indexes = arg.split('/').slice(1).map(Number);
return indexes.every(HDPublicKey.isValidPath);
}
if (typeof arg === 'number') {
return arg >= 0 && arg < HDPublicKey.Hardened;
}
return false;
}
static isValidSerialized(data, network) {
return HDPublicKey.getSerializedError(data, network) === null;
}
static getSerializedError(data, network) {
if (!(typeof data === 'string' || Buffer.isBuffer(data))) {
return new Error('expected buffer or string');
}
if (typeof data === 'string' && !JSUtil.isHexa(data)) {
try {
Base58Check.decode(data);
}
catch (e) {
return new Error('Invalid base58 checksum');
}
}
if (Buffer.isBuffer(data) && data.length !== HDPublicKey.DataSize) {
return new Error('Invalid length');
}
if (typeof data === 'string') {
const decoded = Base58Check.decode(data);
if (decoded.length !== HDPublicKey.DataSize) {
return new Error('Invalid length');
}
}
if (network !== undefined) {
const error = HDPublicKey._validateNetwork(data, network);
if (error) {
return error;
}
}
return null;
}
static _validateNetwork(data, networkArg) {
const network = getNetwork(networkArg);
if (!network) {
return new Error('Invalid network argument');
}
const version = Buffer.isBuffer(data)
? data.subarray(HDPublicKey.VersionStart, HDPublicKey.VersionEnd)
: Buffer.from(Base58Check.decode(data).subarray(HDPublicKey.VersionStart, HDPublicKey.VersionEnd));
if (version.readUInt32BE(0) !== network.xpubkey) {
return new Error('Invalid network');
}
return null;
}
static fromString(arg) {
Preconditions.checkArgument(typeof arg === 'string', 'No valid string was provided');
return new HDPublicKey(arg);
}
static fromObject(arg) {
Preconditions.checkArgument(typeof arg === 'object', 'No valid argument was provided');
return new HDPublicKey(arg);
}
static fromBuffer(arg) {
return new HDPublicKey(arg);
}
_classifyArguments(arg) {
if (typeof arg === 'string') {
return HDPublicKey._transformString(arg);
}
else if (Buffer.isBuffer(arg)) {
return HDPublicKey._transformBuffer(arg);
}
else if (typeof arg === 'object' && arg !== null) {
if ('xpubkey' in arg) {
return HDPublicKey._transformObject(arg);
}
else {
return arg;
}
}
else {
throw new Error('Invalid HDPublicKey data');
}
}
static _transformString(str) {
if (!JSUtil.isHexa(str)) {
return HDPublicKey._transformSerialized(str);
}
return HDPublicKey._transformBuffer(Buffer.from(str, 'hex'));
}
static _transformSerialized(str) {
const buf = Base58Check.decode(str);
return HDPublicKey._transformBuffer(buf);
}
static _transformBuffer(buf) {
if (buf.length !== 78) {
throw new Error('Invalid HDPublicKey buffer length');
}
const version = buf.readUInt32BE(0);
const network = getNetwork(version, 'xpubkey');
if (!network) {
throw new Error('Invalid HDPublicKey network');
}
const depth = buf.readUInt8(4);
const parentFingerPrint = buf.subarray(5, 9);
const childIndex = buf.readUInt32BE(9);
const chainCode = buf.subarray(13, 45);
const publicKeyBuffer = buf.subarray(45, 78);
return {
network,
depth,
parentFingerPrint,
childIndex,
chainCode,
publicKey: PublicKey.fromBuffer(publicKeyBuffer),
};
}
static _transformObject(obj) {
const network = getNetwork(obj.network);
if (!network) {
throw new Error('Invalid network');
}
return {
network,
depth: obj.depth,
parentFingerPrint: Buffer.from(obj.parentFingerPrint, 'hex'),
childIndex: obj.childIndex,
chainCode: Buffer.from(obj.chainCode, 'hex'),
publicKey: PublicKey.fromBuffer(Buffer.from(obj.publicKey, 'hex')),
};
}
_buildFromPrivate(arg) {
const args = {
version: arg._buffers.version,
depth: arg._buffers.depth,
parentFingerPrint: arg._buffers.parentFingerPrint,
childIndex: arg._buffers.childIndex,
chainCode: arg._buffers.chainCode,
publicKey: Point.pointToCompressed(Point.getG().mul(new BN(arg._buffers.privateKey))),
checksum: arg._buffers.checksum,
};
return this._buildFromBuffers(args);
}
_buildFromSerialized(arg) {
const decoded = typeof arg === 'string' ? Base58Check.decode(arg) : arg;
const buffers = {
version: decoded.subarray(HDPublicKey.VersionStart, HDPublicKey.VersionEnd),
depth: decoded.subarray(HDPublicKey.DepthStart, HDPublicKey.DepthEnd),
parentFingerPrint: decoded.subarray(HDPublicKey.ParentFingerPrintStart, HDPublicKey.ParentFingerPrintEnd),
childIndex: decoded.subarray(HDPublicKey.ChildIndexStart, HDPublicKey.ChildIndexEnd),
chainCode: decoded.subarray(HDPublicKey.ChainCodeStart, HDPublicKey.ChainCodeEnd),
publicKey: decoded.subarray(HDPublicKey.PublicKeyStart, HDPublicKey.PublicKeyEnd),
checksum: decoded.subarray(HDPublicKey.ChecksumStart, HDPublicKey.ChecksumEnd),
xpubkey: typeof arg === 'string' ? Buffer.from(arg) : arg,
};
return this._buildFromBuffers(buffers);
}
_buildFromBuffers(arg) {
HDPublicKey._validateBufferArguments(arg);
JSUtil.defineImmutable(this, {
_buffers: arg,
});
const sequence = [
arg.version,
arg.depth,
arg.parentFingerPrint,
arg.childIndex,
arg.chainCode,
arg.publicKey,
];
const concat = Buffer.concat(sequence);
const checksum = Base58Check.checksum(concat);
if (!arg.checksum || !arg.checksum.length) {
arg.checksum = checksum;
}
else {
if (arg.checksum.toString('hex') !== checksum.toString('hex')) {
throw new Error('Invalid base58 checksum');
}
}
const network = getNetwork(arg.version.readUInt32BE(0));
const xpubkey = Base58Check.encode(Buffer.concat(sequence));
arg.xpubkey = Buffer.from(xpubkey);
const publicKey = new PublicKey(arg.publicKey, { network });
const size = HDPublicKey.ParentFingerPrintSize;
const fingerPrint = Hash.sha256ripemd160(publicKey.toBuffer()).subarray(0, size);
JSUtil.defineImmutable(this, {
xpubkey: xpubkey,
network: network,
depth: arg.depth.readUInt8(0),
publicKey: publicKey,
fingerPrint: fingerPrint,
});
return this;
}
static _validateBufferArguments(arg) {
const checkBuffer = (name, size) => {
const buff = arg[name];
if (!Buffer.isBuffer(buff)) {
throw new Error(`${name} argument is not a buffer, it's ${typeof buff}`);
}
if (buff.length !== size) {
throw new Error(`${name} has not the expected size: found ${buff.length}, expected ${size}`);
}
};
checkBuffer('version', HDPublicKey.VersionSize);
checkBuffer('depth', HDPublicKey.DepthSize);
checkBuffer('parentFingerPrint', HDPublicKey.ParentFingerPrintSize);
checkBuffer('childIndex', HDPublicKey.ChildIndexSize);
checkBuffer('chainCode', HDPublicKey.ChainCodeSize);
checkBuffer('publicKey', HDPublicKey.PublicKeySize);
if (arg.checksum && arg.checksum.length) {
checkBuffer('checksum', HDPublicKey.CheckSumSize);
}
}
_buildFromObject(arg) {
const buffers = {
version: Buffer.alloc(4),
depth: typeof arg.depth === 'number'
? Buffer.from([arg.depth])
: Buffer.alloc(1),
parentFingerPrint: typeof arg.parentFingerPrint === 'number'
? Buffer.from([arg.parentFingerPrint])
: Buffer.isBuffer(arg.parentFingerPrint)
? arg.parentFingerPrint
: Buffer.alloc(4),
childIndex: Buffer.alloc(4),
chainCode: typeof arg.chainCode === 'string'
? Buffer.from(arg.chainCode, 'hex')
: Buffer.isBuffer(arg.chainCode)
? arg.chainCode
: Buffer.alloc(32),
publicKey: typeof arg.publicKey === 'string'
? Buffer.from(arg.publicKey, 'hex')
: Buffer.isBuffer(arg.publicKey)
? arg.publicKey
: arg.publicKey?.toBuffer() || Buffer.alloc(33),
checksum: undefined,
};
if (arg.network) {
const network = getNetwork(arg.network);
if (network) {
buffers.version.writeUInt32BE(network.xpubkey, 0);
}
}
if (typeof arg.childIndex === 'number') {
buffers.childIndex.writeUInt32BE(arg.childIndex, 0);
}
return this._buildFromBuffers(buffers);
}
derive(arg, hardened) {
return this.deriveChild(arg, hardened);
}
deriveChild(arg, hardened) {
if (typeof arg === 'number') {
return this._deriveWithNumber(arg, hardened);
}
else if (typeof arg === 'string') {
return this._deriveFromString(arg);
}
else {
throw new Error('Invalid derivation argument');
}
}
_deriveWithNumber(index, hardened) {
if (index >= HDPublicKey.Hardened || hardened) {
throw new Error('Cannot derive hardened keys from public key');
}
if (index < 0) {
throw new Error('Invalid path');
}
const indexBuffer = Buffer.alloc(4);
indexBuffer.writeUInt32BE(index, 0);
const data = Buffer.concat([this.publicKey.toBuffer(), indexBuffer]);
const hash = Hash.sha512hmac(data, this._buffers.chainCode);
const leftPart = new BN(hash.subarray(0, 32));
const chainCode = hash.subarray(32, 64);
let publicKey;
try {
publicKey = PublicKey.fromPoint(Point.getG().mul(leftPart).add(this.publicKey.point));
}
catch (e) {
return this._deriveWithNumber(index + 1);
}
const derived = new HDPublicKey({
network: this.network,
depth: this.depth + 1,
parentFingerPrint: this.fingerPrint,
childIndex: index,
chainCode: chainCode,
publicKey: publicKey,
});
return derived;
}
_deriveFromString(path) {
if (path.includes("'")) {
throw new Error('Cannot derive hardened keys from public key');
}
else if (!HDPublicKey.isValidPath(path)) {
throw new Error('Invalid path');
}
const indexes = path.split('/').slice(1).map(Number);
const derived = indexes.reduce((prev, index) => {
return prev._deriveWithNumber(index);
}, this);
return derived;
}
toString() {
return this.xpubkey.toString();
}
toBuffer() {
return Buffer.from(this._buffers.xpubkey || Buffer.alloc(0));
}
toObject() {
return {
network: this.network.name,
depth: this.depth,
fingerPrint: this.fingerPrint.toString('hex'),
parentFingerPrint: this._buffers.parentFingerPrint.toString('hex'),
childIndex: this._buffers.childIndex.readUInt32BE(0),
chainCode: this._buffers.chainCode.toString('hex'),
publicKey: this.publicKey.toString(),
xpubkey: this.xpubkey.toString(),
};
}
toJSON() {
return JSON.stringify(this.toObject());
}
inspect() {
return '<HDPublicKey: ' + this.xpubkey + '>';
}
}