UNPKG

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
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 + '>'; } }