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.
401 lines
12 KiB
401 lines
12 KiB
/*!
|
|
* lib/wallet/wallet-service.js
|
|
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
|
|
*/
|
|
'use strict'
|
|
|
|
const util = require('../util')
|
|
const Logger = require('../logger')
|
|
const db = require('../db/mysql-db-wrapper')
|
|
const hdaService = require('../bitcoin/hd-accounts-service')
|
|
const hdaHelper = require('../bitcoin/hd-accounts-helper')
|
|
const network = require('../bitcoin/network')
|
|
const activeNet = network.network
|
|
const keys = require('../../keys')[network.key]
|
|
const WalletInfo = require('./wallet-info')
|
|
|
|
|
|
/**
|
|
* A singleton providing a wallets service
|
|
*/
|
|
class WalletService {
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
constructor() {}
|
|
|
|
/**
|
|
* Get full wallet information
|
|
* @param {object} active - mapping of active entities
|
|
* @param {object} legacy - mapping of new legacy addresses
|
|
* @param {object} bip49 - mapping of new bip49 addresses
|
|
* @param {object} bip84 - mapping of new bip84 addresses
|
|
* @param {object} pubkeys - mapping of new pubkeys/addresses
|
|
* @returns {Promise}
|
|
*/
|
|
async getFullWalletInfo(active, legacy, bip49, bip84, pubkeys) {
|
|
// Check parameters
|
|
const validParams = this._checkEntities(active, legacy, bip49, bip84, pubkeys)
|
|
|
|
if (!validParams) {
|
|
const info = new WalletInfo()
|
|
const ret = this._formatGetFullWalletInfoResult(info)
|
|
return Promise.resolve(ret)
|
|
}
|
|
|
|
// Merge all entities into active mapping
|
|
active = this._mergeEntities(active, legacy, bip49, bip84, pubkeys)
|
|
|
|
// Initialize a WalletInfo object
|
|
const walletInfo = new WalletInfo(active)
|
|
|
|
try {
|
|
// Add the new xpubs
|
|
await util.seriesCall(legacy.xpubs, this._newBIP44)
|
|
await util.seriesCall(bip49.xpubs, this._newBIP49)
|
|
await util.seriesCall(bip84.xpubs, this._newBIP84)
|
|
// Load hd accounts info
|
|
await walletInfo.ensureHdAccounts()
|
|
await walletInfo.loadHdAccountsInfo()
|
|
// Add the new addresses
|
|
await db.addAddresses(legacy.addrs)
|
|
await db.addAddresses(bip49.addrs)
|
|
await db.addAddresses(bip84.addrs)
|
|
await db.addAddresses(pubkeys.addrs)
|
|
// Ensure addresses exist
|
|
await walletInfo.ensureAddresses()
|
|
// Force import of addresses associated to paynyms
|
|
// if dojo relies on a local index
|
|
if (keys.indexer.active != 'third_party_explorer')
|
|
await this._forceEnsureAddressesForActivePubkeys(active)
|
|
// Filter the addresses
|
|
await walletInfo.filterAddresses()
|
|
// Load the utxos
|
|
await walletInfo.loadUtxos()
|
|
// Load the addresses
|
|
await walletInfo.loadAddressesInfo()
|
|
// Load the most recent transactions
|
|
await walletInfo.loadTransactions(0, null, true)
|
|
// Load feerates
|
|
await walletInfo.loadFeesInfo()
|
|
// Postprocessing
|
|
await walletInfo.postProcessAddresses()
|
|
await walletInfo.postProcessHdAccounts()
|
|
// Format the result
|
|
return this._formatGetFullWalletInfoResult(walletInfo)
|
|
|
|
} catch(e) {
|
|
Logger.error(e, 'WalletService.getWalletInfo()')
|
|
return Promise.reject({status:'error', error:'internal server error'})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get wallet information
|
|
* @deprecated
|
|
* @param {object} active - mapping of active entities
|
|
* @param {object} legacy - mapping of new legacy addresses
|
|
* @param {object} bip49 - mapping of new bip49 addresses
|
|
* @param {object} bip84 - mapping of new bip84 addresses
|
|
* @param {object} pubkeys - mapping of new pubkeys/addresses
|
|
* @returns {Promise}
|
|
*/
|
|
async getWalletInfo(active, legacy, bip49, bip84, pubkeys) {
|
|
// Check parameters
|
|
const validParams = this._checkEntities(active, legacy, bip49, bip84, pubkeys)
|
|
|
|
if (!validParams) {
|
|
const info = new WalletInfo()
|
|
const ret = this._formatGetWalletInfoResult(info)
|
|
return Promise.resolve(ret)
|
|
}
|
|
|
|
// Merge all entities into active mapping
|
|
active = this._mergeEntities(active, legacy, bip49, bip84, pubkeys)
|
|
|
|
// Initialize a WalletInfo object
|
|
const walletInfo = new WalletInfo(active)
|
|
|
|
try {
|
|
// Add the new xpubs
|
|
await util.seriesCall(legacy.xpubs, this._newBIP44)
|
|
await util.seriesCall(bip49.xpubs, this._newBIP49)
|
|
await util.seriesCall(bip84.xpubs, this._newBIP84)
|
|
// Load hd accounts info
|
|
await walletInfo.ensureHdAccounts()
|
|
await walletInfo.loadHdAccountsInfo()
|
|
// Add the new addresses
|
|
await db.addAddresses(legacy.addrs)
|
|
await db.addAddresses(bip49.addrs)
|
|
await db.addAddresses(bip84.addrs)
|
|
await db.addAddresses(pubkeys.addrs)
|
|
// Ensure addresses exist
|
|
await walletInfo.ensureAddresses()
|
|
// Force import of addresses associated to paynyms
|
|
// if dojo relies on a local index
|
|
if (keys.indexer.active != 'third_party_explorer')
|
|
await this._forceEnsureAddressesForActivePubkeys(active)
|
|
// Filter the address and load them
|
|
await walletInfo.filterAddresses()
|
|
await walletInfo.loadAddressesInfo()
|
|
// Load the most recent transactions
|
|
await walletInfo.loadTransactions(0, null, true)
|
|
// Postprocessing
|
|
await walletInfo.postProcessAddresses()
|
|
await walletInfo.postProcessHdAccounts()
|
|
// Format the result
|
|
return this._formatGetWalletInfoResult(walletInfo)
|
|
|
|
} catch(e) {
|
|
Logger.error(e, 'WalletService.getWalletInfo()')
|
|
return Promise.reject({status:'error', error:'internal server error'})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares the result to be returned by getFullWalletInfo()
|
|
* @param {WalletInfo} info
|
|
* @returns {object}
|
|
*/
|
|
_formatGetFullWalletInfoResult(info) {
|
|
let ret = info.toPojo()
|
|
|
|
delete ret['n_tx']
|
|
|
|
ret.addresses = ret.addresses.map(x => {
|
|
delete x['derivation']
|
|
delete x['created']
|
|
return x
|
|
})
|
|
|
|
return ret
|
|
}
|
|
|
|
/**
|
|
* Prepares the result to be returned by getWalletInfo()
|
|
* @deprecated
|
|
* @param {WalletInfo} info
|
|
* @returns {object}
|
|
*/
|
|
_formatGetWalletInfoResult(info) {
|
|
let ret = info.toPojo()
|
|
|
|
delete ret['n_tx']
|
|
delete ret['unspent_outputs']
|
|
delete ret['info']['fees']
|
|
|
|
ret.addresses = ret.addresses.map(x => {
|
|
delete x['derivation']
|
|
delete x['created']
|
|
return x
|
|
})
|
|
|
|
return ret
|
|
}
|
|
|
|
/**
|
|
* Get wallet unspent outputs
|
|
* @deprecated
|
|
* @param {object} active - mapping of active entities
|
|
* @param {object} legacy - mapping of new legacy addresses
|
|
* @param {object} bip49 - mapping of new bip49 addresses
|
|
* @param {object} bip84 - mapping of new bip84 addresses
|
|
* @param {object} pubkeys - mapping of new pubkeys/addresses
|
|
* @returns {Promise}
|
|
*/
|
|
async getWalletUtxos(active, legacy, bip49, bip84, pubkeys) {
|
|
const ret = {
|
|
unspent_outputs: []
|
|
}
|
|
|
|
// Check parameters
|
|
const validParams = this._checkEntities(active, legacy, bip49, bip84, pubkeys)
|
|
if (!validParams)
|
|
return Promise.resolve(ret)
|
|
|
|
// Merge all entities into active mapping
|
|
active = this._mergeEntities(active, legacy, bip49, bip84, pubkeys)
|
|
|
|
// Initialize a WalletInfo object
|
|
const walletInfo = new WalletInfo(active)
|
|
|
|
try {
|
|
// Add the new xpubs
|
|
await util.seriesCall(legacy.xpubs, this._newBIP44)
|
|
await util.seriesCall(bip49.xpubs, this._newBIP49)
|
|
await util.seriesCall(bip84.xpubs, this._newBIP84)
|
|
// Ensure hd accounts exist
|
|
await walletInfo.ensureHdAccounts()
|
|
// Add the new addresses
|
|
await db.addAddresses(legacy.addrs)
|
|
await db.addAddresses(bip49.addrs)
|
|
await db.addAddresses(bip84.addrs)
|
|
await db.addAddresses(pubkeys.addrs)
|
|
// Ensure addresses exist
|
|
await walletInfo.ensureAddresses()
|
|
// Force import of addresses associated to paynyms
|
|
// if dojo relies on a local index
|
|
if (keys.indexer.active != 'third_party_explorer')
|
|
await this._forceEnsureAddressesForActivePubkeys(active)
|
|
// Filter the addresses
|
|
await walletInfo.filterAddresses()
|
|
// Load the utxos
|
|
await walletInfo.loadUtxos()
|
|
// Postprocessing
|
|
await walletInfo.postProcessAddresses()
|
|
await walletInfo.postProcessHdAccounts()
|
|
// Format the result
|
|
ret.unspent_outputs = walletInfo.unspentOutputs
|
|
return ret
|
|
|
|
} catch(e) {
|
|
Logger.error(e, 'WalletService.getWalletUtxos()')
|
|
return Promise.reject({status: 'error', error: 'internal server error'})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a subset of wallet transactions
|
|
* @param {object} entities - mapping of active entities
|
|
* @param {integer} page - page of transactions to be returned
|
|
* @param {integer} count - number of transactions returned per page
|
|
* @returns {Promise}
|
|
*/
|
|
async getWalletTransactions(entities, page, count) {
|
|
const ret = {
|
|
n_tx: 0,
|
|
page: page,
|
|
n_tx_page: count,
|
|
txs: []
|
|
}
|
|
|
|
// Check parameters
|
|
if (entities.xpubs.length == 0 && entities.addrs.length == 0)
|
|
return ret
|
|
|
|
// Initialize a WalletInfo object
|
|
const walletInfo = new WalletInfo(entities)
|
|
|
|
try {
|
|
// Filter the addresses
|
|
await walletInfo.filterAddresses()
|
|
// Load the number of transactions
|
|
await walletInfo.loadNbTransactions()
|
|
// Load the requested page of transactions
|
|
await walletInfo.loadTransactions(page, count, false)
|
|
// Postprocessing
|
|
await walletInfo.postProcessAddresses()
|
|
await walletInfo.postProcessHdAccounts()
|
|
// Format the result
|
|
ret.n_tx = walletInfo.nTx
|
|
ret.txs = walletInfo.txs
|
|
return ret
|
|
|
|
} catch(e) {
|
|
Logger.error(e, 'WalletService.getWalletTransactions()')
|
|
return Promise.reject({status:'error', error:'internal server error'})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force addresses derived from an active pubkey to be stored in database
|
|
* @param {object} active - mapping of active entities
|
|
* @returns {Promise}
|
|
*/
|
|
async _forceEnsureAddressesForActivePubkeys(active) {
|
|
const filteredAddrs = []
|
|
for (let i in active.addrs) {
|
|
if (active.pubkeys[i]) {
|
|
filteredAddrs.push(active.addrs[i])
|
|
}
|
|
}
|
|
return db.addAddresses(filteredAddrs)
|
|
}
|
|
|
|
/**
|
|
* Check entities
|
|
* @param {object} active - mapping of active entities
|
|
* @param {object} legacy - mapping of new legacy addresses
|
|
* @param {object} bip49 - mapping of new bip49 addresses
|
|
* @param {object} bip84 - mapping of new bip84 addresses
|
|
* @param {object} pubkeys - mapping of new pubkeys/addresses
|
|
* @returns {boolean} return true if conditions are met, false otherwise
|
|
*/
|
|
_checkEntities(active, legacy, bip49, bip84, pubkeys) {
|
|
const allEmpty = active.xpubs.length == 0
|
|
&& active.addrs.length == 0
|
|
&& legacy.xpubs.length == 0
|
|
&& legacy.addrs.length == 0
|
|
&& pubkeys.addrs.length == 0
|
|
&& bip49.xpubs.length == 0
|
|
&& bip84.xpubs.length == 0
|
|
|
|
return !allEmpty
|
|
}
|
|
|
|
/**
|
|
* Merge all entities into active mapping
|
|
* @param {object} active - mapping of active entities
|
|
* @param {object} legacy - mapping of new legacy entities
|
|
* @param {object} bip49 - mapping of new bip49 entities
|
|
* @param {object} bip84 - mapping of new bip84 entities
|
|
* @param {object} pubkeys - mapping of new pubkeys
|
|
*/
|
|
_mergeEntities(active, legacy, bip49, bip84, pubkeys) {
|
|
// Put all xpub into active.xpubs
|
|
active.xpubs = active.xpubs
|
|
.concat(legacy.xpubs)
|
|
.concat(bip49.xpubs)
|
|
.concat(bip84.xpubs)
|
|
|
|
// Put addresses and pubkeys into active
|
|
// but avoid duplicates
|
|
for (let source of [legacy, pubkeys]) {
|
|
for (let idxSource in source.addrs) {
|
|
const addr = source.addrs[idxSource]
|
|
const pubkey = source.pubkeys[idxSource]
|
|
const idxActive = active.addrs.indexOf(addr)
|
|
|
|
if (idxActive == -1) {
|
|
active.addrs.push(addr)
|
|
active.pubkeys.push(pubkey)
|
|
} else if (pubkey) {
|
|
active.pubkeys[idxActive] = pubkey
|
|
}
|
|
}
|
|
}
|
|
|
|
return active
|
|
}
|
|
|
|
/**
|
|
* Create a new BIP44 hd account into the database
|
|
* @param {string} xpub
|
|
* @returns {Promise}
|
|
*/
|
|
async _newBIP44(xpub) {
|
|
return hdaService.createHdAccount(xpub, hdaHelper.BIP44)
|
|
}
|
|
|
|
/**
|
|
* Create a new BIP49 hd account into the database
|
|
* @param {string} xpub
|
|
* @returns {Promise}
|
|
*/
|
|
async _newBIP49(xpub) {
|
|
return hdaService.createHdAccount(xpub, hdaHelper.BIP49)
|
|
}
|
|
|
|
/**
|
|
* Create a new BIP84 hd account into the database
|
|
* @param {string} xpub
|
|
* @returns {Promise}
|
|
*/
|
|
async _newBIP84(xpub) {
|
|
return hdaService.createHdAccount(xpub, hdaHelper.BIP84)
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = new WalletService()
|
|
|