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.
 
 
 
 
 
 

320 lines
8.4 KiB

/*!
* lib/wallet/wallet-info.js
* Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
*/
'use strict'
const db = require('../db/mysql-db-wrapper')
const util = require('../util')
const rpcLatestBlock = require('../bitcoind-rpc/latest-block')
const rpcFees = require('../bitcoind-rpc/fees')
const addrService = require('../bitcoin/addresses-service')
const HdAccountInfo = require('./hd-account-info')
const AddressInfo = require('./address-info')
/**
* A class storing information about a (full|partial) wallet
* Provides a set of methods allowing to retrieve specific information
*/
class WalletInfo {
/**
* Constructor
* @param {object} entities - wallet entities (hdaccounts, addresses, pubkeys)
*/
constructor(entities) {
// Initializes wallet properties
this.entities = entities
this.wallet = {
finalBalance: 0
}
this.info = {
fees: {},
latestBlock: {
height: rpcLatestBlock.height,
hash: rpcLatestBlock.hash,
time: rpcLatestBlock.time,
}
}
this.addresses = []
this.txs = []
this.unspentOutputs = []
this.nTx = 0
}
/**
* Ensure hd accounts exist in database
* @returns {Promise}
*/
async ensureHdAccounts() {
return util.parallelCall(this.entities.xpubs, async xpub => {
const hdaInfo = new HdAccountInfo(xpub)
return hdaInfo.ensureHdAccount()
})
}
/**
* Load information about the hd accounts
* @returns {Promise}
*/
async loadHdAccountsInfo() {
return util.parallelCall(this.entities.xpubs, async xpub => {
const hdaInfo = new HdAccountInfo(xpub)
await hdaInfo.loadInfo()
this.wallet.finalBalance += hdaInfo.finalBalance
this.addresses.push(hdaInfo)
})
}
/**
* Ensure addresses exist in database
* @returns {Promise}
*/
async ensureAddresses() {
const importAddrs = []
const addrIdMap = await db.getAddressesIds(this.entities.addrs)
for (let addr of this.entities.addrs) {
if (!addrIdMap[addr])
importAddrs.push(addr)
}
// Import new addresses
return addrService.restoreAddresses(importAddrs, true)
}
/**
* Filter addresses that belong to an active hd account
* @returns {Promise}
*/
async filterAddresses() {
const res = await db.getXpubByAddresses(this.entities.addrs)
for (let addr in res) {
let xpub = res[addr]
if (this.entities.xpubs.indexOf(xpub) > -1) {
let i = this.entities.addrs.indexOf(addr)
if (i > -1) {
this.entities.addrs.splice(i, 1)
this.entities.pubkeys.splice(i, 1)
}
}
}
}
/**
* Load information about the addresses
* @returns {Promise}
*/
async loadAddressesInfo() {
return util.parallelCall(this.entities.addrs, async address => {
const addrInfo = new AddressInfo(address)
await addrInfo.loadInfo()
this.wallet.finalBalance += addrInfo.finalBalance
this.addresses.push(addrInfo)
})
}
/**
* Loads a partial list of transactions for this wallet
* @param {integer} page - page index
* @param {integer} count - number of transactions per page
* @param {boolean} txBalance - True if past wallet balance
* should be computed for each transaction
* @returns {Promise}
*/
async loadTransactions(page, count, txBalance) {
this.txs = await db.getTxsByAddrAndXpubs(
this.entities.addrs,
this.entities.xpubs,
page,
count
)
if (txBalance) {
// Computes wallet balance after each transaction
let balance = this.wallet.finalBalance
for (let i = 0; i < this.txs.length; i++) {
this.txs[i].balance = balance
balance -= this.txs[i].result
}
}
}
/**
* Loads the number of transactions for this wallet
* @returns {Promise}
*/
async loadNbTransactions() {
const nbTxs = await db.getAddrAndXpubsNbTransactions(
this.entities.addrs,
this.entities.xpubs
)
if (nbTxs !== null)
this.nTx = nbTxs
}
/**
* Loads tinfo about the fee rates
* @returns {Promise}
*/
async loadFeesInfo() {
this.info.fees = await rpcFees.getFees()
}
/**
* Loads the list of unspent outputs for this wallet
* @returns {Promise}
*/
async loadUtxos() {
// Load the utxos for the hd accounts
await util.parallelCall(this.entities.xpubs, async xpub => {
const hdaInfo = new HdAccountInfo(xpub)
const utxos = await hdaInfo.loadUtxos()
for (let utxo of utxos)
this.unspentOutputs.push(utxo)
})
// Load the utxos for the addresses
const utxos = await db.getUnspentOutputs(this.entities.addrs)
for (let utxo of utxos) {
const conf =
(utxo.blockHeight == null)
? 0
: (rpcLatestBlock.height - utxo.blockHeight + 1)
const entry = {
tx_hash: utxo.txnTxid,
tx_output_n: utxo.outIndex,
tx_version: utxo.txnVersion,
tx_locktime: utxo.txnLocktime,
value: utxo.outAmount,
script: utxo.outScript,
addr: utxo.addrAddress,
confirmations: conf
}
this.unspentOutputs.push(entry)
}
// Order the utxos
this.unspentOutputs.sort((a,b) => b.confirmations - a.confirmations)
}
/**
* Post process addresses and public keys
*/
postProcessAddresses() {
for (let b = 0; b < this.entities.pubkeys.length; b++) {
const pk = this.entities.pubkeys[b]
if (pk) {
const address = this.entities.addrs[b]
// Add pubkeys in this.addresses
for (let c = 0; c < this.addresses.length; c++) {
if (address == this.addresses[c].address)
this.addresses[c].pubkey = pk
}
// Add pubkeys in this.txs
for (let d = 0; d < this.txs.length; d++) {
// inputs
for (let e = 0; e < this.txs[d].inputs.length; e++) {
if (address == this.txs[d].inputs[e].prev_out.addr)
this.txs[d].inputs[e].prev_out.pubkey = pk
}
// outputs
for (let e = 0; e < this.txs[d].out.length; e++) {
if (address == this.txs[d].out[e].addr)
this.txs[d].out[e].pubkey = pk
}
}
// Add pubkeys in this.unspentOutputs
for (let f = 0; f < this.unspentOutputs.length; f++) {
if (address == this.unspentOutputs[f].addr) {
this.unspentOutputs[f].pubkey = pk
}
}
}
}
}
/**
* Post process hd accounts (xpubs translations)
*/
postProcessHdAccounts() {
for (let b = 0; b < this.entities.xpubs.length; b++) {
const entityXPub = this.entities.xpubs[b]
const entityYPub = this.entities.ypubs[b]
const entityZPub = this.entities.zpubs[b]
if (entityYPub || entityZPub) {
const tgtXPub = entityYPub ? entityYPub : entityZPub
// Translate xpub => ypub/zpub in this.addresses
for (let c = 0; c < this.addresses.length; c++) {
if (entityXPub == this.addresses[c].address)
this.addresses[c].address = tgtXPub
}
// Translate xpub => ypub/zpub in this.txs
for (let d = 0; d < this.txs.length; d++) {
// inputs
for (let e = 0; e < this.txs[d].inputs.length; e++) {
const xpub = this.txs[d].inputs[e].prev_out.xpub
if (xpub && (xpub.m == entityXPub))
this.txs[d].inputs[e].prev_out.xpub.m = tgtXPub
}
// outputs
for (let e = 0; e < this.txs[d].out.length; e++) {
const xpub = this.txs[d].out[e].xpub
if (xpub && (xpub.m == entityXPub))
this.txs[d].out[e].xpub.m = tgtXPub
}
}
// Translate xpub => ypub/zpub in this.unspentOutputs
for (let f = 0; f < this.unspentOutputs.length; f++) {
const xpub = this.unspentOutputs[f].xpub
if (xpub && (xpub.m == entityXPub)) {
this.unspentOutputs[f].xpub.m = tgtXPub
}
}
}
}
}
/**
* Return a plain old js object with wallet properties
* @returns {object}
*/
toPojo() {
return {
wallet: {
final_balance: this.wallet.finalBalance
},
info: {
fees: this.info.fees,
latest_block: this.info.latestBlock
},
addresses: this.addresses.map(a => a.toPojo()),
txs: this.txs,
unspent_outputs: this.unspentOutputs,
n_tx: this.nTx
}
}
}
module.exports = WalletInfo