You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
257 lines
7.7 KiB
257 lines
7.7 KiB
/*!
|
|
* lib/bitcoin/hd-accounts-service.js
|
|
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
|
|
*/
|
|
'use strict'
|
|
|
|
const _ = require('lodash')
|
|
const errors = require('../errors')
|
|
const Logger = require('../logger')
|
|
const db = require('../db/mysql-db-wrapper')
|
|
const network = require('../bitcoin/network')
|
|
const gap = require('../../keys/')[network.key].gap
|
|
const remote = require('../remote-importer/remote-importer')
|
|
const hdaHelper = require('./hd-accounts-helper')
|
|
const addrHelper = require('./addresses-helper')
|
|
|
|
|
|
/**
|
|
* A singleton providing a HD Accounts service
|
|
*/
|
|
class HDAccountsService {
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
constructor() {}
|
|
|
|
|
|
/**
|
|
* Create a new hd account in db
|
|
* @param {string} xpub - xpub
|
|
* @param {int} scheme - derivation scheme
|
|
* @returns {Promise} returns true if success, false otherwise
|
|
*/
|
|
async createHdAccount(xpub, scheme) {
|
|
try {
|
|
await this.newHdAccount(xpub, scheme)
|
|
return true
|
|
} catch(e) {
|
|
const isInvalidXpub = (e == errors.xpub.INVALID || e == errors.xpub.PRIVKEY)
|
|
const isLockedXpub = (e == errors.xpub.LOCKED)
|
|
const err = (isInvalidXpub || isLockedXpub) ? e : errors.xpub.CREATE
|
|
Logger.error(e, 'HdAccountsService : createHdAccount()' + err)
|
|
return Promise.reject(err)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Restore a hd account in db
|
|
* @param {string} xpub - xpub
|
|
* @param {int} scheme - derivation scheme
|
|
* @param {bool} forceOverride - force override of scheme even if hd account is locked
|
|
* @returns {Promise}
|
|
*/
|
|
async restoreHdAccount(xpub, scheme, forceOverride) {
|
|
let isLocked
|
|
|
|
// Check if hd accounts exists in db and is locked
|
|
try {
|
|
const account = await db.getHDAccount(xpub)
|
|
const info = hdaHelper.classify(account.hdType)
|
|
isLocked = info.locked
|
|
} catch(e) {}
|
|
|
|
// Override derivation scheme if needed
|
|
await this.derivationOverrideCheck(xpub, scheme, forceOverride)
|
|
|
|
// Import the hd account
|
|
await remote.importHDAccount(xpub, scheme)
|
|
|
|
// Lock the hd account if needed
|
|
if (isLocked)
|
|
return this.lockHdAccount(xpub, true)
|
|
}
|
|
|
|
/**
|
|
* Lock a hd account
|
|
* @param {string} xpub - xpub
|
|
* @param {boolean} lock - true for locking, false for unlocking
|
|
* @returns {Promise} returns the derivation type as a string
|
|
*/
|
|
async lockHdAccount(xpub, lock) {
|
|
try {
|
|
const account = await db.getHDAccount(xpub)
|
|
|
|
const hdType = account.hdType
|
|
const info = hdaHelper.classify(hdType)
|
|
|
|
if (info.locked === lock)
|
|
return hdaHelper.typeString(hdType)
|
|
|
|
await db.setLockHDAccountType(xpub, lock)
|
|
|
|
const type = hdaHelper.makeType(hdType, lock)
|
|
return hdaHelper.typeString(type)
|
|
|
|
} catch(e) {
|
|
const err = (e == errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
|
|
return Promise.reject(err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a hd account
|
|
* @param {string} xpub - xpub
|
|
* @returns {Promise}
|
|
*/
|
|
async deleteHdAccount(xpub) {
|
|
try {
|
|
await db.deleteHDAccount(xpub)
|
|
} catch(e) {
|
|
const err = (e == errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
|
|
return Promise.reject(err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new xpub in db
|
|
* @param {string} xpub - xpub
|
|
* @param {string} scheme - derivation scheme
|
|
* @returns {Promise}
|
|
*/
|
|
async newHdAccount(xpub, scheme) {
|
|
// Get the HDNode bitcoinjs object.
|
|
// Throws if xpub is actually a private key
|
|
const HDNode = hdaHelper.getNode(xpub)
|
|
|
|
if (HDNode === null)
|
|
throw errors.xpub.INVALID
|
|
|
|
await this.derivationOverrideCheck(xpub, scheme)
|
|
await db.ensureHDAccountId(xpub, scheme)
|
|
|
|
let segwit = ''
|
|
|
|
if (scheme == hdaHelper.BIP49)
|
|
segwit = ' SegWit (BIP49)'
|
|
else if (scheme == hdaHelper.BIP84)
|
|
segwit = ' SegWit (BIP84)'
|
|
|
|
Logger.info(`HdAccountsService : Created HD Account: ${xpub}${segwit}`)
|
|
|
|
const externalPrm = hdaHelper.deriveAddresses(xpub, 0, _.range(gap.external), scheme)
|
|
const internalPrm = hdaHelper.deriveAddresses(xpub, 1, _.range(gap.internal), scheme)
|
|
|
|
const addresses = _.flatten(await Promise.all([externalPrm, internalPrm]))
|
|
|
|
return db.addAddressesToHDAccount(xpub, addresses)
|
|
}
|
|
|
|
/**
|
|
* Rescan the blockchain for a hd account
|
|
* @param {string} xpub - xpub
|
|
* @param {integer} gapLimit - (optional) gap limit for derivation
|
|
* @param {integer} startIndex - (optional) rescan shall start from this index
|
|
* @returns {Promise}
|
|
*/
|
|
async rescan(xpub, gapLimit, startIndex) {
|
|
// Force rescan
|
|
remote.clearGuard(xpub)
|
|
|
|
try {
|
|
const account = await db.getHDAccount(xpub)
|
|
await remote.importHDAccount(xpub, account.hdType, gapLimit, startIndex)
|
|
} catch(e) {
|
|
return Promise.reject(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a xpub is currently being imported or rescanned by Dojo
|
|
* Returns true if import/rescan is in progress, otherwise returns false
|
|
* @param {string} xpub - xpub
|
|
* @returns {Promise}
|
|
*/
|
|
importInProgress(xpub) {
|
|
return remote.importInProgress(xpub)
|
|
}
|
|
|
|
/**
|
|
* Check if we try to override an existing xpub
|
|
* Delete the old xpub from db if it's the case
|
|
* @param {string} xpub - xpub
|
|
* @param {string} scheme - derivation scheme
|
|
* @param {boolean} forceOverride - force override of scheme even if hd account is locked
|
|
* (default = false)
|
|
* @returns {Promise}
|
|
*/
|
|
async derivationOverrideCheck(xpub, scheme, forceOverride) {
|
|
let account
|
|
|
|
// Nothing to do here if hd account doesn't exist in db
|
|
try {
|
|
account = await db.getHDAccount(xpub)
|
|
} catch(e) {
|
|
return Promise.resolve()
|
|
}
|
|
|
|
try {
|
|
const info = hdaHelper.classify(account.hdType)
|
|
// If this account is already known in the database,
|
|
// check for a derivation scheme mismatch
|
|
if (info.type != scheme) {
|
|
if (info.locked && !forceOverride) {
|
|
Logger.info(`HdAccountsService : Attempted override on locked account: ${xpub}`)
|
|
return Promise.reject(errors.xpub.LOCKED)
|
|
} else {
|
|
Logger.info(`HdAccountsService : Derivation scheme override: ${xpub}`)
|
|
return db.deleteHDAccount(xpub)
|
|
}
|
|
}
|
|
} catch(e) {
|
|
Logger.error(e, 'HDAccountsService : derivationOverrideCheck()')
|
|
return Promise.reject(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify that a given message has been signed
|
|
* with the first external key of a known xpub/ypub/zpub
|
|
*
|
|
* @param {string} xpub - xpub
|
|
* @param {string} address - address used to sign the message
|
|
* @param {string} sig - signature of the message
|
|
* @param {string} msg - signed message
|
|
* @param {integer} scheme - derivation scheme to be used for the xpub
|
|
* @returns {Promise} returns the xpub if signature is valid, otherwise returns an error
|
|
*/
|
|
async verifyXpubSignature(xpub, address, sig, msg, scheme) {
|
|
// Derive addresses (P2PKH addresse used for signature + expected address)
|
|
const sigAddressRecord = await hdaHelper.deriveAddresses(xpub, 1, [0], hdaHelper.BIP44)
|
|
const sigAddress = sigAddressRecord[0].address
|
|
|
|
const expectedAddressRecord = await hdaHelper.deriveAddresses(xpub, 1, [0], scheme)
|
|
const expectedAddress = expectedAddressRecord[0].address
|
|
|
|
try {
|
|
// Check that xpub exists in db
|
|
await db.getHDAccountId(xpub)
|
|
// Check the signature
|
|
if (!addrHelper.verifySignature(msg, sigAddress, sig))
|
|
return Promise.reject(errors.sig.INVSIG)
|
|
// Check that adresses match
|
|
if (address != expectedAddress)
|
|
return Promise.reject(errors.sig.INVADDR)
|
|
// Return the corresponding xpub
|
|
return xpub
|
|
} catch(err) {
|
|
const ret = (err == errors.db.ERROR_NO_HD_ACCOUNT) ? errors.get.UNKNXPUB : errors.generic.DB
|
|
return Promise.reject(ret)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = new HDAccountsService()
|
|
|