bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
637 lines • 23.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.V8 = void 0;
const async = __importStar(require("async"));
const crypto = __importStar(require("crypto"));
const lodash_1 = __importDefault(require("lodash"));
const request = __importStar(require("request-promise-native"));
const io = __importStar(require("socket.io-client"));
const config_1 = __importDefault(require("../../config"));
const index_1 = require("../chain/index");
const common_1 = require("../common");
const logger_1 = __importDefault(require("../logger"));
const client_1 = require("./v8/client");
const $ = require('preconditions').singleton();
const Bitcore = require('bitcore-lib');
const Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash'),
eth: Bitcore,
matic: Bitcore,
xrp: Bitcore,
doge: require('bitcore-lib-doge'),
ltc: require('bitcore-lib-ltc'),
arb: Bitcore,
op: Bitcore,
base: Bitcore,
sol: Bitcore
};
const Constants = common_1.Common.Constants, Defaults = common_1.Common.Defaults, Utils = common_1.Common.Utils;
const { sortDesc } = Utils;
function v8network(bwsNetwork, chain = 'btc') {
if (Utils.getGenericName(bwsNetwork) == 'livenet')
return 'mainnet';
if (Utils.getGenericName(bwsNetwork) == 'testnet' && config_1.default.blockchainExplorerOpts?.[chain.toLowerCase()]?.[Utils.getNetworkName(chain.toLowerCase(), 'testnet')]?.regtestEnabled) {
return 'regtest';
}
return bwsNetwork;
}
class V8 {
constructor(opts) {
$.checkArgument(opts);
$.checkArgument(Utils.checkValueInCollection(opts.chain, Constants.CHAINS));
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS[opts.chain]));
$.checkArgument(opts.url);
this.apiPrefix = opts.apiPrefix == null ? '/api' : opts.apiPrefix;
this.chain = opts.chain;
this.network = opts.network || 'livenet';
this.v8network = v8network(this.network, this.chain);
this.addressFormat = this.chain == 'bch' ? 'cashaddr' : null;
this.chainNetwork = `/${this.chain.toUpperCase()}/${this.v8network}`;
this.apiPrefix += this.chainNetwork;
this.host = opts.url;
this.userAgent = opts.userAgent || 'bws';
this.baseUrl = this.host + this.apiPrefix;
this.request = opts.request || request;
this.Client = opts.client || client_1.Client || require('./v8/client');
}
_getClient() {
return new this.Client({
baseUrl: this.baseUrl
});
}
_getAuthClient(wallet) {
$.checkState(wallet.beAuthPrivateKey2, 'Failed state: wallet.beAuthPrivateKey2 at <_getAuthClient()>');
return new this.Client({
baseUrl: this.baseUrl,
authKey: Bitcore_[this.chain].PrivateKey(wallet.beAuthPrivateKey2)
});
}
addAddresses(wallet, addresses, cb, opts) {
const client = this._getAuthClient(wallet);
const payload = addresses.map(a => ({ address: a }));
if (opts?.reprocess) {
const c = this._getAuthClient({ beAuthPrivateKey2: config_1.default.blockchainExplorerOpts.socketApiKey });
opts.reprocess = c.sign({ method: 'reprocess', url: 'http://thisdontmatter.com/addAddresses' + wallet.beAuthPublicKey2, payload });
}
const k = 'addAddresses' + !!opts.reprocess + addresses.length;
const perfKey = getPerformanceKey(k);
console.time(perfKey);
client
.importAddresses({
payload,
pubKey: wallet.beAuthPublicKey2,
reprocess: opts?.reprocess
})
.then(ret => {
console.timeEnd(perfKey);
return cb(null, ret);
})
.catch(err => {
return cb(err);
});
}
register(wallet, cb) {
if (wallet.chain != this.chain || wallet.network != this.network) {
return cb(new Error('Network chain or network mismatch'));
}
const client = this._getAuthClient(wallet);
const payload = {
name: wallet.id,
pubKey: wallet.beAuthPublicKey2
};
client
.register({
authKey: wallet.beAuthPrivateKey2,
payload
})
.then(ret => {
return cb(null, ret);
})
.catch(cb);
}
async getBalance(wallet, cb) {
const client = this._getAuthClient(wallet);
const { tokenAddress, multisigContractAddress } = wallet;
client
.getBalance({ pubKey: wallet.beAuthPublicKey2, payload: {}, tokenAddress, multisigContractAddress })
.then(ret => {
return cb(null, ret);
})
.catch(cb);
}
getConnectionInfo() {
return 'V8 (' + this.chain + '/' + this.v8network + ') @ ' + this.host;
}
_transformUtxos(utxos, bcheight) {
$.checkState(bcheight > 0, 'Failed state: No BC height passed to _transformUtxos()');
const ret = lodash_1.default.map(lodash_1.default.reject(utxos, x => {
return x.spentHeight && x.spentHeight <= -3;
}), x => {
const u = {
address: x.address,
satoshis: x.value,
amount: x.value / 1e8,
scriptPubKey: x.script,
txid: x.mintTxid,
vout: x.mintIndex,
locked: false,
confirmations: x.mintHeight > 0 && bcheight >= x.mintHeight ? bcheight - x.mintHeight + 1 : 0,
spent: x.spentHeight != -2
};
return u;
});
return ret;
}
getUtxos(wallet, height, cb, params = {}) {
$.checkArgument(cb);
const client = this._getAuthClient(wallet);
const perfKey = getPerformanceKey('V8getUtxos');
console.time(perfKey);
client
.getCoins({
pubKey: wallet.beAuthPublicKey2,
payload: {},
...params
})
.then(utxos => {
console.timeEnd(perfKey);
return cb(null, this._transformUtxos(utxos, height));
})
.catch(cb);
}
getCoinsForTx(txId, cb) {
$.checkArgument(cb);
const client = this._getClient();
const perfKey = getPerformanceKey('V8getCoinsForTx');
console.time(perfKey);
client
.getCoinsForTx({ txId, payload: {} })
.then(coins => {
console.timeEnd(perfKey);
return cb(null, coins);
})
.catch(cb);
}
getCheckData(wallet, cb) {
const client = this._getAuthClient(wallet);
const perfKey = getPerformanceKey('WalletCheck');
console.time(perfKey);
client
.getCheckData({ pubKey: wallet.beAuthPublicKey2, payload: {} })
.then(checkInfo => {
console.timeEnd(perfKey);
return cb(null, checkInfo);
})
.catch(cb);
}
broadcast(rawTx, cb, count = 0) {
const payload = {
rawTx,
network: this.v8network,
chain: this.chain.toUpperCase()
};
const client = this._getClient();
client
.broadcast({ payload })
.then(ret => {
if (!ret.txid) {
return cb(new Error('Error broadcasting'));
}
return cb(null, ret.txid);
})
.catch(err => {
if (count > 3) {
logger_1.default.error('[v8.js] FINAL Broadcast error: %o', err);
return cb(err);
}
else {
count++;
setTimeout(() => {
logger_1.default.info('[v8.js] Retrying broadcast after %o', count * Defaults.BROADCAST_RETRY_TIME);
return this.broadcast(rawTx, cb, count);
}, count * Defaults.BROADCAST_RETRY_TIME);
}
});
}
getTransaction(txid, cb) {
logger_1.default.debug('[v8.js] GET TX %o', txid);
const client = this._getClient();
client
.getTx({ txid })
.then(tx => {
if (!tx || JSON.stringify(tx) === '{}') {
return cb();
}
return cb(null, tx);
})
.catch(err => {
if (err.statusCode == '404') {
return cb();
}
else {
return cb(err);
}
});
}
getAddressUtxos(address, height, cb) {
logger_1.default.debug('[v8.js] GET ADDR UTXO, %o, %o', address, height);
const client = this._getClient();
client
.getAddressTxos({ address, unspent: true })
.then(utxos => {
return cb(null, this._transformUtxos(utxos, height));
})
.catch(cb);
}
getTransactions(wallet, startBlock, cb) {
const perfKey = getPerformanceKey('V8getTxs');
console.time(perfKey);
if (startBlock) {
logger_1.default.debug(`getTxs: startBlock ${startBlock}`);
}
else {
logger_1.default.debug('getTxs: from 0');
}
const client = this._getAuthClient(wallet);
let acum = '', broken;
const opts = {
includeMempool: true,
pubKey: wallet.beAuthPublicKey2,
payload: {},
startBlock: undefined,
tokenAddress: wallet.tokenAddress,
multisigContractAddress: wallet.multisigContractAddress
};
if (startBlock != null && !isNaN(startBlock))
opts.startBlock = startBlock;
const txStream = client.listTransactions(opts);
txStream.on('data', raw => {
acum = acum + raw.toString();
});
txStream.on('end', () => {
if (broken) {
return;
}
const txs = [], unconf = [];
for (const rawTx of acum.split(/\r?\n/)) {
if (!rawTx)
continue;
let tx;
try {
tx = JSON.parse(rawTx);
}
catch (e) {
logger_1.default.error('[v8.js] Error at JSON.parse:' + e + ' Parsing:' + rawTx + ':');
return cb(e);
}
if (tx.value)
tx.amount = tx.satoshis / 1e8;
if (tx.height >= 0)
txs.push(tx);
else if (tx.height >= -2)
unconf.push(tx);
}
console.timeEnd(perfKey);
return cb(null, lodash_1.default.flatten(sortDesc(unconf, 'blockTime').concat(txs.reverse())));
});
txStream.on('error', e => {
logger_1.default.error('[v8.js] Error: %o', e);
broken = true;
return cb(e);
});
}
getAddressActivity(address, cb) {
const url = this.baseUrl + '/address/' + address + '/txs?limit=1';
logger_1.default.debug('[v8.js] CHECKING ADDRESS ACTIVITY %o', url);
this.request
.get(url, {})
.then(ret => {
return cb(null, ret !== '[]');
})
.catch(err => {
return cb(err);
});
}
getTransactionCount(address, cb) {
const url = this.baseUrl + '/address/' + address + '/txs/count';
logger_1.default.debug('[v8.js] CHECKING ADDRESS NONCE %o', url);
this.request
.get(url, {})
.then(ret => {
ret = JSON.parse(ret);
return cb(null, ret.nonce);
})
.catch(err => {
return cb(err);
});
}
estimateGas(opts, cb) {
const url = this.baseUrl + '/gas';
logger_1.default.debug('[v8.js] CHECKING GAS LIMIT %o', url);
this.request
.post(url, { body: opts, json: true })
.then(gasLimit => {
gasLimit = JSON.parse(gasLimit);
return cb(null, gasLimit);
})
.catch(err => {
return cb(err);
});
}
getMultisigContractInstantiationInfo(opts, cb) {
const url = `${this.baseUrl}/ethmultisig/${opts.sender}/instantiation/${opts.txId}`;
logger_1.default.debug('[v8.js] CHECKING CONTRACT INSTANTIATION INFO %o', url);
this.request
.get(url, {})
.then(contractInstantiationInfo => {
contractInstantiationInfo = JSON.parse(contractInstantiationInfo);
return cb(null, contractInstantiationInfo);
})
.catch(err => {
return cb(err);
});
}
getMultisigContractInfo(opts, cb) {
const url = this.baseUrl + '/ethmultisig/info/' + opts.multisigContractAddress;
logger_1.default.debug('[v8.js] CHECKING CONTRACT INFO %o', url);
this.request
.get(url, {})
.then(contractInfo => {
contractInfo = JSON.parse(contractInfo);
return cb(null, contractInfo);
})
.catch(err => {
return cb(err);
});
}
getTokenContractInfo(opts, cb) {
const url = this.baseUrl + '/token/' + opts.tokenAddress;
logger_1.default.debug('[v8.js] CHECKING CONTRACT INFO %o', url);
this.request
.get(url, {})
.then(contractInfo => {
contractInfo = JSON.parse(contractInfo);
return cb(null, contractInfo);
})
.catch(err => {
return cb(err);
});
}
getTokenAllowance(opts, cb) {
const url = this.baseUrl + '/token/' + opts.tokenAddress + '/allowance/' + opts.ownerAddress + '/for/' + opts.spenderAddress;
logger_1.default.debug('[v8.js] CHECKING TOKEN ALLOWANCE %o', url);
this.request
.get(url, {})
.then(allowance => {
allowance = JSON.parse(allowance);
return cb(null, allowance);
})
.catch(err => {
return cb(err);
});
}
getMultisigTxpsInfo(opts, cb) {
const url = this.baseUrl + '/ethmultisig/txps/' + opts.multisigContractAddress;
logger_1.default.debug('[v8.js] CHECKING CONTRACT TXPS INFO %o', url);
this.request
.get(url, {})
.then(multisigTxpsInfo => {
multisigTxpsInfo = JSON.parse(multisigTxpsInfo);
return cb(null, multisigTxpsInfo);
})
.catch(err => {
return cb(err);
});
}
estimateFee(nbBlocks, cb) {
nbBlocks = nbBlocks || [1, 2, 6, 24];
const result = {};
async.each(nbBlocks, (x, icb) => {
const url = this.baseUrl + '/fee/' + x;
this.request
.get(url, {})
.then(ret => {
try {
ret = JSON.parse(ret);
if (!lodash_1.default.isUndefined(ret.blocks) && ret.blocks != x) {
logger_1.default.info(`[v8.js] Ignoring response for ${x}: %o`, ret?.body || ret);
return icb();
}
result[x] = ret.feerate;
}
catch (e) {
logger_1.default.warn('[v8.js] Fee error: %o', e);
}
return icb();
})
.catch(err => {
return icb(err);
});
}, err => {
if (err) {
return cb(err);
}
return cb(null, result);
});
}
estimateFeeV2(opts, cb) {
const txType = opts.txType;
if (Number(txType) !== 2) {
return this.estimateFee(opts, cb);
}
const nbBlocks = Number(opts.nbBlocks) || 2;
const url = this.baseUrl + `/fee/${nbBlocks}?txType=${txType}`;
let result;
this.request
.get(url, {})
.then(ret => {
try {
ret = JSON.parse(ret);
result = ret.feerate;
}
catch (e) {
logger_1.default.warn('[v8.js] Fee error: %o', e);
}
return cb(null, result);
})
.catch(err => {
return cb(err);
});
}
estimatePriorityFee(opts, cb) {
const percentile = opts.percentile;
let result;
const url = this.baseUrl + `/priorityFee/${percentile}`;
this.request
.get(url, {})
.then(ret => {
try {
ret = JSON.parse(ret);
result = ret.feerate;
}
catch (e) {
logger_1.default.warn('[v8.js] Priority fee error: %o', e);
}
return cb(null, result);
})
.catch(err => {
return cb(err);
});
}
getBlockchainHeight(cb) {
const url = this.baseUrl + '/block/tip';
this.request
.get(url, {})
.then(ret => {
try {
ret = JSON.parse(ret);
return cb(null, ret.height, ret.hash);
}
catch (err) {
return cb(new Error('Could not get height from block explorer'));
}
})
.catch(cb);
}
getTxidsInBlock(blockHash, cb) {
const url = this.baseUrl + '/tx/?blockHash=' + blockHash;
this.request
.get(url, {})
.then(ret => {
try {
ret = JSON.parse(ret);
const res = lodash_1.default.map(ret, 'txid');
return cb(null, res);
}
catch (err) {
return cb(new Error('Could not get height from block explorer'));
}
})
.catch(cb);
}
getReserve(cb) {
if (this._cachedReserveTs && Date.now() - this._cachedReserveTs < 20 * 60 * 1000) {
return cb(null, this._cachedReserve);
}
const url = this.baseUrl + '/reserve';
this.request
.get(url, {})
.then(ret => {
try {
ret = JSON.parse(ret);
if (ret.reserve != null) {
this._cachedReserve = ret.reserve;
this._cachedReserveTs = Date.now();
}
return cb(null, ret.reserve ?? Defaults.MIN_XRP_BALANCE);
}
catch (err) {
logger_1.default.error('[v8.js] Error getting reserve: %o', err);
return cb(null, Defaults.MIN_XRP_BALANCE);
}
})
.catch(cb);
}
initSocket(callbacks) {
logger_1.default.info('V8 connecting socket at:' + this.host);
const walletsSocket = io.connect(this.host, { transports: ['websocket'] });
const blockSocket = io.connect(this.host, { transports: ['websocket'] });
const getAuthPayload = host => {
const authKey = config_1.default.blockchainExplorerOpts.socketApiKey;
if (!authKey)
throw new Error('provide authKey');
const authKeyObj = new Bitcore.PrivateKey(authKey);
const pubKey = authKeyObj.toPublicKey().toString();
const authClient = new client_1.Client({ baseUrl: host, authKey: authKeyObj });
const payload = { method: 'socket', url: host };
const authPayload = { pubKey, message: authClient.getMessage(payload), signature: authClient.sign(payload) };
return authPayload;
};
blockSocket.on('connect', () => {
logger_1.default.info(`Connected to block ${this.getConnectionInfo()}`);
blockSocket.emit('room', `${this.chainNetwork}/inv`);
});
blockSocket.on('connect_error', () => {
logger_1.default.error(`Error connecting to ${this.getConnectionInfo()}`);
});
blockSocket.on('block', data => {
return callbacks.onBlock(data.hash);
});
walletsSocket.on('connect', () => {
logger_1.default.info(`Connected to wallets ${this.getConnectionInfo()}`);
walletsSocket.emit('room', `${this.chainNetwork}/wallets`, getAuthPayload(this.host));
});
walletsSocket.on('connect_error', () => {
logger_1.default.error(`Error connecting to ${this.getConnectionInfo()} ${this.chainNetwork}`);
});
walletsSocket.on('failure', err => {
logger_1.default.error(`Error joining room ${err.message} ${this.chainNetwork}`);
});
walletsSocket.on('coin', data => {
if (!data || !data.coin)
return;
const notification = index_1.ChainService.onCoin(this.chain, data.coin);
if (!notification)
return;
return callbacks.onIncomingPayments(notification);
});
walletsSocket.on('tx', data => {
if (!data || !data.tx)
return;
const notification = index_1.ChainService.onTx(this.chain, data.tx);
if (!notification)
return;
return callbacks.onIncomingPayments(notification);
});
}
}
exports.V8 = V8;
const _parseErr = (err, res) => {
if (err) {
logger_1.default.warn('[v8.js] V8 raw error: %o', err);
return 'V8 Error';
}
logger_1.default.warn('[v8.js] ' + res.request.href + ' Returned Status: ' + res.statusCode);
return 'Error querying the blockchain';
};
const getPerformanceKey = (name) => {
return name + '-' + crypto.randomBytes(5).toString('hex');
};
//# sourceMappingURL=v8.js.map