/*! * client.js - http client for wallets * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('assert'); const Network = require('../protocol/network'); const AsyncObject = require('../utils/asyncobject'); const RPCClient = require('./rpcclient'); const request = require('./request'); /** * Bcoin HTTP client. * @alias module:http.Client * @constructor * @param {String} uri * @param {Object?} options */ function HTTPClient(options) { if (!(this instanceof HTTPClient)) return new HTTPClient(options); if (!options) options = {}; if (typeof options === 'string') options = { uri: options }; AsyncObject.call(this); this.options = options; this.network = Network.get(options.network); this.uri = options.uri || `http://localhost:${this.network.rpcPort}`; this.socket = null; this.apiKey = options.apiKey; this.auth = options.auth; this.rpc = new RPCClient(options); } Object.setPrototypeOf(HTTPClient.prototype, AsyncObject.prototype); /** * Open the client, wait for socket to connect. * @alias HTTPClient#open * @returns {Promise} */ HTTPClient.prototype._open = async function _open() { let IOClient; try { IOClient = require('socket.io-client'); } catch (e) { ; } if (!IOClient) return; this.socket = new IOClient(this.uri, { transports: ['websocket'], forceNew: true }); this.socket.on('error', (err) => { this.emit('error', err); }); this.socket.on('version', (info) => { if (info.network !== this.network.type) this.emit('error', new Error('Wrong network.')); }); this.socket.on('wallet tx', (details) => { this.emit('tx', details); }); this.socket.on('wallet confirmed', (details) => { this.emit('confirmed', details); }); this.socket.on('wallet unconfirmed', (details) => { this.emit('unconfirmed', details); }); this.socket.on('wallet conflict', (details) => { this.emit('conflict', details); }); this.socket.on('wallet updated', (details) => { this.emit('updated', details); }); this.socket.on('wallet address', (receive) => { this.emit('address', receive); }); this.socket.on('wallet balance', (balance) => { this.emit('balance', balance); }); await this.onConnect(); await this.sendAuth(); }; /** * Close the client, wait for the socket to close. * @alias HTTPClient#close * @returns {Promise} */ HTTPClient.prototype._close = function _close() { if (!this.socket) return Promise.resolve(); this.socket.disconnect(); this.socket = null; return Promise.resolve(); }; /** * Wait for websocket connection. * @private * @returns {Promise} */ HTTPClient.prototype.onConnect = function onConnect() { return new Promise((resolve, reject) => { this.socket.once('connect', resolve); }); }; /** * Wait for websocket auth. * @private * @returns {Promise} */ HTTPClient.prototype.sendAuth = function sendAuth() { return new Promise((resolve, reject) => { this.socket.emit('auth', this.apiKey, (err) => { if (err) { reject(new Error(err.message)); return; } resolve(); }); }); }; /** * Wait for websocket auth. * @private * @returns {Promise} */ HTTPClient.prototype.sendWalletAuth = function sendWalletAuth() { return new Promise((resolve, reject) => { this.socket.emit('wallet auth', this.apiKey, (err) => { if (err) { reject(new Error(err.message)); return; } resolve(); }); }); }; /** * Wait for websocket disconnection. * @private * @returns {Promise} */ HTTPClient.prototype.onDisconnect = function onDisconnect() { return new Promise((resolve, reject) => { this.socket.once('disconnect', resolve); }); }; /** * Make an http request to endpoint. * @private * @param {String} method * @param {String} endpoint - Path. * @param {Object} json - Body or query depending on method. * @returns {Promise} - Returns Object?. */ HTTPClient.prototype._request = async function _request(method, endpoint, json) { if (this.token) { if (!json) json = {}; json.token = this.token; } let query; if (json && method === 'get') { query = json; json = null; } const res = await request({ method: method, uri: this.uri + endpoint, pool: true, query: query, json: json, auth: { username: 'bitcoinrpc', password: this.apiKey || '' } }); if (res.statusCode === 404) return null; if (res.statusCode === 401) throw new Error('Unauthorized (bad API key).'); if (res.type !== 'json') throw new Error('Bad response (wrong content-type).'); if (!res.body) throw new Error('Bad response (no body).'); if (res.body.error) throw new Error(res.body.error.message); if (res.statusCode !== 200) throw new Error(`Status code: ${res.statusCode}.`); const network = res.headers['x-bcoin-network']; if (network && network !== this.network.type) throw new Error('Bad response (wrong network).'); return res.body; }; /** * Make a GET http request to endpoint. * @private * @param {String} endpoint - Path. * @param {Object} json - Querystring. * @returns {Promise} - Returns Object?. */ HTTPClient.prototype._get = function _get(endpoint, json) { return this._request('get', endpoint, json); }; /** * Make a POST http request to endpoint. * @private * @param {String} endpoint - Path. * @param {Object} json - Body. * @returns {Promise} - Returns Object?. */ HTTPClient.prototype._post = function _post(endpoint, json) { return this._request('post', endpoint, json); }; /** * Make a PUT http request to endpoint. * @private * @param {String} endpoint - Path. * @param {Object} json - Body. * @returns {Promise} - Returns Object?. */ HTTPClient.prototype._put = function _put(endpoint, json) { return this._request('put', endpoint, json); }; /** * Make a DELETE http request to endpoint. * @private * @param {String} endpoint - Path. * @param {Object} json - Body. * @returns {Promise} - Returns Object?. */ HTTPClient.prototype._del = function _del(endpoint, json) { return this._request('delete', endpoint, json); }; /** * Get a mempool snapshot. * @returns {Promise} - Returns {@link Hash}[]. */ HTTPClient.prototype.getMempool = function getMempool() { return this._get('/mempool'); }; /** * Get some info about the server (network and version). * @returns {Promise} - Returns Object. */ HTTPClient.prototype.getInfo = function getInfo() { return this._get('/'); }; /** * Get coins that pertain to an address from the mempool or chain database. * Takes into account spent coins in the mempool. * @param {String} address * @returns {Promise} - Returns {@link Coin}[]. */ HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address) { return this._get(`/coin/address/${address}`); }; /** * Get coins that pertain to addresses from the mempool or chain database. * Takes into account spent coins in the mempool. * @param {String[]} addresses * @returns {Promise} - Returns {@link Coin}[]. */ HTTPClient.prototype.getCoinsByAddresses = function getCoinsByAddresses(addresses) { return this._post('/coin/address', { addresses }); }; /** * Retrieve a coin from the mempool or chain database. * Takes into account spent coins in the mempool. * @param {Hash} hash * @param {Number} index * @returns {Promise} - Returns {@link Coin}. */ HTTPClient.prototype.getCoin = function getCoin(hash, index) { return this._get(`/coin/${hash}/${index}`); }; /** * Retrieve transactions pertaining to an * address from the mempool or chain database. * @param {String} address * @returns {Promise} - Returns {@link TX}[]. */ HTTPClient.prototype.getTXByAddress = function getTXByAddress(address) { return this._get(`/tx/address/${address}`); }; /** * Retrieve transactions pertaining to * addresses from the mempool or chain database. * @param {String[]} addresses * @returns {Promise} - Returns {@link TX}[]. */ HTTPClient.prototype.getTXByAddresses = function getTXByAddresses(addresses) { return this._post('/tx/address', { addresses }); }; /** * Retrieve a transaction from the mempool or chain database. * @param {Hash} hash * @returns {Promise} - Returns {@link TX}. */ HTTPClient.prototype.getTX = function getTX(hash) { return this._get(`/tx/${hash}`); }; /** * Retrieve a block from the chain database. * @param {Hash|Number} block * @returns {Promise} - Returns {@link Block}. */ HTTPClient.prototype.getBlock = function getBlock(block) { return this._get(`/block/${block}`); }; /** * Add a transaction to the mempool and broadcast it. * @param {TX} tx * @returns {Promise} */ HTTPClient.prototype.broadcast = function broadcast(tx) { return this._post('/broadcast', { tx: toHex(tx) }); }; /** * Rescan the chain. * @param {Number} height * @returns {Promise} */ HTTPClient.prototype.rescan = function rescan(height) { return this._post('/wallet/_admin/rescan', { height }); }; /** * Reset the chain. * @param {Number} height * @returns {Promise} */ HTTPClient.prototype.reset = function reset(height) { return this._post('/reset', { height }); }; /** * Resend pending transactions. * @returns {Promise} */ HTTPClient.prototype.resend = function resend() { return this._post('/wallet/_admin/resend', {}); }; /** * Backup the walletdb. * @param {String} path * @returns {Promise} */ HTTPClient.prototype.backup = function backup(path) { return this._post('/wallet/_admin/backup', { path }); }; /** * Listen for events on wallet id. * @param {String} id * @param {String?} token * @returns {Promise} */ HTTPClient.prototype.join = function join(id, token) { if (!this.socket) return Promise.resolve(); return new Promise((resolve, reject) => { this.socket.emit('wallet join', id, token, (err) => { if (err) { reject(new Error(err.message)); return; } resolve(); }); }); }; /** * Unlisten for events on wallet id. * @param {String} id */ HTTPClient.prototype.leave = function leave(id) { if (!this.socket) return Promise.resolve(); return new Promise((resolve, reject) => { this.socket.emit('wallet leave', id, (err) => { if (err) { reject(new Error(err.message)); return; } resolve(); }); }); }; /** * Get list of all wallet IDs. * @returns {Promise} */ HTTPClient.prototype.getWallets = function getWallets() { return this._get('/wallet/_admin/wallets'); }; /** * Create a wallet. * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.createWallet = function createWallet(options) { assert(options.id, 'Must pass an id parameter'); return this._put(`/wallet/${options.id}`, options); }; /** * Get the raw wallet JSON. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getWallet = function getWallet(id) { return this._get(`/wallet/${id}`); }; /** * Get wallet transaction history. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getHistory = function getHistory(id, account) { return this._get(`/wallet/${id}/tx/history`, { account }); }; /** * Get wallet coins. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getCoins = function getCoins(id, account) { return this._get(`/wallet/${id}/coin`, { account }); }; /** * Get all unconfirmed transactions. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getPending = function getPending(id, account) { return this._get(`/wallet/${id}/tx/unconfirmed`, { account }); }; /** * Calculate wallet balance. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getBalance = function getBalance(id, account) { return this._get(`/wallet/${id}/balance`, { account }); }; /** * Get last N wallet transactions. * @param {String} id * @param {Number} limit - Max number of transactions. * @returns {Promise} */ HTTPClient.prototype.getLast = function getLast(id, account, limit) { return this._get(`/wallet/${id}/tx/last`, { account, limit }); }; /** * Get wallet transactions by timestamp range. * @param {String} id * @param {Object} options * @param {Number} options.start - Start time. * @param {Number} options.end - End time. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. * @returns {Promise} */ HTTPClient.prototype.getRange = function getRange(id, account, options) { const body = { account: account, start: options.start, end: options.end , limit: options.limit, reverse: options.reverse }; return this._get(`/wallet/${id}/tx/range`, body); }; /** * Get transaction (only possible if the transaction * is available in the wallet history). * @param {String} id * @param {Hash} hash * @returns {Promise} */ HTTPClient.prototype.getWalletTX = function getWalletTX(id, hash) { return this._get(`/wallet/${id}/tx/${hash}`); }; /** * Get wallet blocks. * @param {String} id * @param {Number} height * @returns {Promise} */ HTTPClient.prototype.getWalletBlocks = function getWalletBlocks(id) { return this._get(`/wallet/${id}/block`); }; /** * Get wallet block. * @param {String} id * @param {Number} height * @returns {Promise} */ HTTPClient.prototype.getWalletBlock = function getWalletBlock(id, height) { return this._get(`/wallet/${id}/block/${height}`); }; /** * Get unspent coin (only possible if the transaction * is available in the wallet history). * @param {String} id * @param {Hash} hash * @param {Number} index * @returns {Promise} */ HTTPClient.prototype.getWalletCoin = function getWalletCoin(id, account, hash, index) { return this._get(`/wallet/${id}/coin/${hash}/${index}`, { account }); }; /** * Create a transaction, fill, sign, and broadcast. * @param {String} id * @param {Object} options * @param {String} options.address * @param {Amount} options.value * @returns {Promise} */ HTTPClient.prototype.send = function send(id, options) { const body = Object.assign({}, options); if (!body.outputs) body.outputs = []; body.outputs = body.outputs.map((output) => { return { value: output.value, address: output.address, script: toHex(output.script) }; }); return this._post(`/wallet/${id}/send`, body); }; /** * Generate a new token. * @param {(String|Buffer)?} passphrase * @returns {Promise} */ HTTPClient.prototype.retoken = async function retoken(id, passphrase) { const body = await this._post(`/wallet/${id}/retoken`, { passphrase }); return body.token; }; /** * Change or set master key's passphrase. * @param {(String|Buffer)?} old * @param {String|Buffer} new_ * @returns {Promise} */ HTTPClient.prototype.setPassphrase = function setPassphrase(id, old, new_) { const body = { old: old, passphrase: new_ }; return this._post(`/wallet/${id}/passphrase`, body); }; /** * Create a transaction, fill. * @param {String} id * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.createTX = function createTX(id, options) { const body = Object.assign({}, options); if (!body.outputs) body.outputs = []; body.outputs = body.outputs.map((output) => { return { value: output.value, address: output.address, script: toHex(output.script) }; }); return this._post(`/wallet/${id}/create`, body); }; /** * Sign a transaction. * @param {String} id * @param {TX} tx * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.sign = function sign(id, tx, options) { const body = Object.assign({}, options); body.tx = toHex(tx); return this._post(`/wallet/${id}/sign`, body); }; /** * @param {String} id * @param {Number} now - Current time. * @param {Number} age - Age delta (delete transactions older than `now - age`). * @returns {Promise} */ HTTPClient.prototype.zapWallet = function zapWallet(id, account, age) { return this._post(`/wallet/${id}/zap`, { account, age }); }; /** * Get wallet key. * @param {String} id * @param {String} address * @returns {Promise} */ HTTPClient.prototype.getKey = function getKey(id, address) { return this._get(`/wallet/${id}/key/${address}`); }; /** * Get wallet key WIF dump. * @param {String} id * @param {String} address * @param {String?} passphrase * @returns {Promise} */ HTTPClient.prototype.getWIF = function getWIF(id, address, passphrase) { return this._get(`/wallet/${id}/wif/${address}`, { passphrase }); }; /** * Add a public account/purpose key to the wallet for multisig. * @param {String} id * @param {(String|Number)?} account * @param {Base58String} key - Account (bip44) or * Purpose (bip45) key (can be in base58 form). * @returns {Promise} */ HTTPClient.prototype.addSharedKey = function addSharedKey(id, account, key) { const body = { account: account, accountKey: key }; return this._put(`/wallet/${id}/shared-key`, body); }; /** * Remove a public account/purpose key to the wallet for multisig. * @param {String} id * @param {(String|Number)?} account * @param {Base58String} key - Account (bip44) or Purpose * (bip45) key (can be in base58 form). * @returns {Promise} */ HTTPClient.prototype.removeSharedKey = function removeSharedKey(id, account, key) { const body = { account: account, accountKey: key }; return this._del(`/wallet/${id}/shared-key`, body); }; /** * Import private key. * @param {String} id * @param {Number|String} account * @param {String} key * @returns {Promise} */ HTTPClient.prototype.importPrivate = function importPrivate(id, account, key) { const body = { account: account, privateKey: key }; return this._post(`/wallet/${id}/import`, body); }; /** * Import public key. * @param {String} id * @param {Number|String} account * @param {String} key * @returns {Promise} */ HTTPClient.prototype.importPublic = function importPublic(id, account, key) { const body = { account: account, publicKey: key }; return this._post(`/wallet/${id}/import`, body); }; /** * Import address. * @param {String} id * @param {Number|String} account * @param {String} address * @returns {Promise} */ HTTPClient.prototype.importAddress = function importAddress(id, account, address) { return this._post(`/wallet/${id}/import`, { account, address }); }; /** * Lock a coin. * @param {String} id * @param {String} hash * @param {Number} index * @returns {Promise} */ HTTPClient.prototype.lockCoin = function lockCoin(id, hash, index) { return this._put(`/wallet/${id}/coin/locked`, { hash, index }); }; /** * Unlock a coin. * @param {String} id * @param {String} hash * @param {Number} index * @returns {Promise} */ HTTPClient.prototype.unlockCoin = function unlockCoin(id, hash, index) { return this._del(`/wallet/${id}/coin/locked`, { hash, index }); }; /** * Get locked coins. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getLocked = function getLocked(id) { return this._get(`/wallet/${id}/coin/locked`); }; /** * Lock wallet. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.lock = function lock(id) { return this._post(`/wallet/${id}/lock`, {}); }; /** * Unlock wallet. * @param {String} id * @param {String} passphrase * @param {Number} timeout * @returns {Promise} */ HTTPClient.prototype.unlock = function unlock(id, passphrase, timeout) { return this._post(`/wallet/${id}/unlock`, { passphrase, timeout }); }; /** * Resend pending wallet transactions. * @returns {Promise} */ HTTPClient.prototype.resendWallet = function resendWallet(id) { return this._post(`/wallet/${id}/resend`, {}); }; /** * Get wallet accounts. * @param {String} id * @returns {Promise} - Returns Array. */ HTTPClient.prototype.getAccounts = function getAccounts(id) { return this._get(`/wallet/${id}/account`); }; /** * Get wallet master key. * @param {String} id * @returns {Promise} */ HTTPClient.prototype.getMaster = function getMaster(id) { return this._get(`/wallet/${id}/master`); }; /** * Get wallet account. * @param {String} id * @param {String} account * @returns {Promise} */ HTTPClient.prototype.getAccount = function getAccount(id, account) { return this._get(`/wallet/${id}/account/${account}`); }; /** * Create account. * @param {String} id * @param {String} name * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.createAccount = function createAccount(id, name, options) { return this._put(`/wallet/${id}/account/${name}`, options || {}); }; /** * Create address. * @param {String} id * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.createAddress = function createAddress(id, options) { if (!options) options = {}; if (typeof options === 'string') options = { account: options }; return this._post(`/wallet/${id}/address`, options); }; /** * Create change address. * @param {String} id * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.createChange = function createChange(id, options) { if (!options) options = {}; if (typeof options === 'string') options = { account: options }; return this._post(`/wallet/${id}/change`, options); }; /** * Create nested address. * @param {String} id * @param {Object} options * @returns {Promise} */ HTTPClient.prototype.createNested = function createNested(id, options) { if (!options) options = {}; if (typeof options === 'string') options = { account: options }; return this._post(`/wallet/${id}/nested`, options); }; /* * Helpers */ function toHex(obj) { if (!obj) return null; if (obj.toRaw) obj = obj.toRaw(); if (Buffer.isBuffer(obj)) obj = obj.toString('hex'); return obj; } /* * Expose */ module.exports = HTTPClient;