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.
310 lines
8.1 KiB
310 lines
8.1 KiB
6 years ago
|
/*!
|
||
|
* 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 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 = {
|
||
|
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.seriesCall(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.seriesCall(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.seriesCall(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 the list of unspent outputs for this wallet
|
||
|
* @returns {Promise}
|
||
|
*/
|
||
|
async loadUtxos() {
|
||
|
// Load the utxos for the hd accounts
|
||
|
await util.seriesCall(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: {
|
||
|
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
|