/*!
* rpc.js - bitcoind-compatible json rpc for bcoin.
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
const assert = require('bsert');
const {format} = require('util');
const bweb = require('bweb');
const {Lock} = require('bmutex');
const fs = require('bfile');
const Validator = require('bval');
const {BufferMap, BufferSet} = require('buffer-map');
const util = require('../utils/util');
const messageUtil = require('../utils/message');
const Amount = require('../btc/amount');
const Script = require('../script/script');
const Address = require('../primitives/address');
const KeyRing = require('../primitives/keyring');
const MerkleBlock = require('../primitives/merkleblock');
const MTX = require('../primitives/mtx');
const Outpoint = require('../primitives/outpoint');
const Output = require('../primitives/output');
const TX = require('../primitives/tx');
const consensus = require('../protocol/consensus');
const pkg = require('../pkg');
const common = require('./common');
const {BlockMeta} = require('./records');
const RPCBase = bweb.RPC;
const RPCError = bweb.RPCError;
/*
* Constants
*/
const errs = {
// Standard JSON-RPC 2.0 errors
INVALID_REQUEST: bweb.errors.INVALID_REQUEST,
METHOD_NOT_FOUND: bweb.errors.METHOD_NOT_FOUND,
INVALID_PARAMS: bweb.errors.INVALID_PARAMS,
INTERNAL_ERROR: bweb.errors.INTERNAL_ERROR,
PARSE_ERROR: bweb.errors.PARSE_ERROR,
// General application defined errors
MISC_ERROR: -1,
FORBIDDEN_BY_SAFE_MODE: -2,
TYPE_ERROR: -3,
INVALID_ADDRESS_OR_KEY: -5,
OUT_OF_MEMORY: -7,
INVALID_PARAMETER: -8,
DATABASE_ERROR: -20,
DESERIALIZATION_ERROR: -22,
VERIFY_ERROR: -25,
VERIFY_REJECTED: -26,
VERIFY_ALREADY_IN_CHAIN: -27,
IN_WARMUP: -28,
// Wallet errors
WALLET_ERROR: -4,
WALLET_INSUFFICIENT_FUNDS: -6,
WALLET_INVALID_ACCOUNT_NAME: -11,
WALLET_KEYPOOL_RAN_OUT: -12,
WALLET_UNLOCK_NEEDED: -13,
WALLET_PASSPHRASE_INCORRECT: -14,
WALLET_WRONG_ENC_STATE: -15,
WALLET_ENCRYPTION_FAILED: -16,
WALLET_ALREADY_UNLOCKED: -17
};
/**
* Wallet RPC
* @alias module:wallet.RPC
* @extends bweb.RPC
*/
class RPC extends RPCBase {
/**
* Create an RPC.
* @param {WalletDB} wdb
*/
constructor(node) {
super();
assert(node, 'RPC requires a WalletDB.');
this.wdb = node.wdb;
this.network = node.network;
this.logger = node.logger.context('wallet-rpc');
this.client = node.client;
this.locker = new Lock();
this.wallet = null;
this.init();
}
getCode(err) {
switch (err.type) {
case 'RPCError':
return err.code;
case 'ValidationError':
return errs.TYPE_ERROR;
case 'EncodingError':
return errs.DESERIALIZATION_ERROR;
case 'FundingError':
return errs.WALLET_INSUFFICIENT_FUNDS;
default:
return errs.INTERNAL_ERROR;
}
}
handleCall(cmd, query) {
this.logger.debug('Handling RPC call: %s.', cmd.method);
}
init() {
this.add('help', this.help);
this.add('stop', this.stop);
this.add('fundrawtransaction', this.fundRawTransaction);
this.add('resendwallettransactions', this.resendWalletTransactions);
this.add('abandontransaction', this.abandonTransaction);
this.add('addmultisigaddress', this.addMultisigAddress);
this.add('addwitnessaddress', this.addWitnessAddress);
this.add('backupwallet', this.backupWallet);
this.add('dumpprivkey', this.dumpPrivKey);
this.add('dumpwallet', this.dumpWallet);
this.add('encryptwallet', this.encryptWallet);
this.add('getaddressinfo', this.getAddressInfo);
this.add('getaccountaddress', this.getAccountAddress);
this.add('getaccount', this.getAccount);
this.add('getaddressesbyaccount', this.getAddressesByAccount);
this.add('getbalance', this.getBalance);
this.add('getnewaddress', this.getNewAddress);
this.add('getrawchangeaddress', this.getRawChangeAddress);
this.add('getreceivedbyaccount', this.getReceivedByAccount);
this.add('getreceivedbyaddress', this.getReceivedByAddress);
this.add('gettransaction', this.getTransaction);
this.add('getunconfirmedbalance', this.getUnconfirmedBalance);
this.add('getwalletinfo', this.getWalletInfo);
this.add('importprivkey', this.importPrivKey);
this.add('importwallet', this.importWallet);
this.add('importaddress', this.importAddress);
this.add('importprunedfunds', this.importPrunedFunds);
this.add('importpubkey', this.importPubkey);
this.add('keypoolrefill', this.keyPoolRefill);
this.add('listaccounts', this.listAccounts);
this.add('listaddressgroupings', this.listAddressGroupings);
this.add('listlockunspent', this.listLockUnspent);
this.add('listreceivedbyaccount', this.listReceivedByAccount);
this.add('listreceivedbyaddress', this.listReceivedByAddress);
this.add('listsinceblock', this.listSinceBlock);
this.add('listtransactions', this.listTransactions);
this.add('listunspent', this.listUnspent);
this.add('lockunspent', this.lockUnspent);
this.add('move', this.move);
this.add('sendfrom', this.sendFrom);
this.add('sendmany', this.sendMany);
this.add('sendtoaddress', this.sendToAddress);
this.add('setaccount', this.setAccount);
this.add('settxfee', this.setTXFee);
this.add('signmessage', this.signMessage);
this.add('walletlock', this.walletLock);
this.add('walletpassphrasechange', this.walletPassphraseChange);
this.add('walletpassphrase', this.walletPassphrase);
this.add('removeprunedfunds', this.removePrunedFunds);
this.add('selectwallet', this.selectWallet);
this.add('getmemoryinfo', this.getMemoryInfo);
this.add('setloglevel', this.setLogLevel);
}
async help(args, _help) {
if (args.length === 0)
return `Select a command:\n${Object.keys(this.calls).join('\n')}`;
const json = {
method: args[0],
params: []
};
return await this.execute(json, true);
}
async stop(args, help) {
if (help || args.length !== 0)
throw new RPCError(errs.MISC_ERROR, 'stop');
this.wdb.close();
return 'Stopping.';
}
async fundRawTransaction(args, help) {
if (help || args.length < 1 || args.length > 2) {
throw new RPCError(errs.MISC_ERROR,
'fundrawtransaction "hexstring" ( options )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const data = valid.buf(0);
const options = valid.obj(1);
if (!data)
throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.');
const tx = MTX.fromRaw(data);
if (tx.outputs.length === 0) {
throw new RPCError(errs.INVALID_PARAMETER,
'TX must have at least one output.');
}
let rate = null;
let change = null;
if (options) {
const valid = new Validator(options);
rate = valid.ufixed('feeRate', 8);
change = valid.str('changeAddress');
if (change)
change = parseAddress(change, this.network);
}
await wallet.fund(tx, {
rate: rate,
changeAddress: change
});
return {
hex: tx.toRaw().toString('hex'),
changepos: tx.changeIndex,
fee: Amount.btc(tx.getFee(), true)
};
}
/*
* Wallet
*/
async resendWalletTransactions(args, help) {
if (help || args.length !== 0)
throw new RPCError(errs.MISC_ERROR, 'resendwallettransactions');
const wallet = this.wallet;
const txs = await wallet.resend();
const hashes = [];
for (const tx of txs)
hashes.push(tx.txid());
return hashes;
}
async addMultisigAddress(args, help) {
// Impossible to implement in bcoin (no address book).
throw new Error('Not implemented.');
}
async addWitnessAddress(args, help) {
// Unlikely to be implemented.
throw new Error('Not implemented.');
}
async backupWallet(args, help) {
const valid = new Validator(args);
const dest = valid.str(0);
if (help || args.length !== 1 || !dest)
throw new RPCError(errs.MISC_ERROR, 'backupwallet "destination"');
await this.wdb.backup(dest);
return null;
}
async dumpPrivKey(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'dumpprivkey "bitcoinaddress"');
const wallet = this.wallet;
const valid = new Validator(args);
const addr = valid.str(0, '');
const hash = parseHash(addr, this.network);
const ring = await wallet.getPrivateKey(hash);
if (!ring)
throw new RPCError(errs.MISC_ERROR, 'Key not found.');
return ring.toSecret(this.network);
}
async dumpWallet(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'dumpwallet "filename"');
const wallet = this.wallet;
const valid = new Validator(args);
const file = valid.str(0);
if (!file)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
const tip = await this.wdb.getTip();
const time = util.date();
const out = [
format('# Wallet Dump created by Bcoin %s', pkg.version),
format('# * Created on %s', time),
format('# * Best block at time of backup was %d (%s).',
tip.height, util.revHex(tip.hash)),
format('# * File: %s', file),
''
];
const hashes = await wallet.getAddressHashes();
for (const hash of hashes) {
const ring = await wallet.getPrivateKey(hash);
if (!ring)
continue;
const addr = ring.getAddress('string', this.network);
let fmt = '%s %s label= addr=%s';
if (ring.branch === 1)
fmt = '%s %s change=1 addr=%s';
const str = format(fmt, ring.toSecret(this.network), time, addr);
out.push(str);
}
out.push('');
out.push('# End of dump');
out.push('');
const dump = out.join('\n');
if (fs.unsupported)
return dump;
await fs.writeFile(file, dump, 'utf8');
return null;
}
async encryptWallet(args, help) {
const wallet = this.wallet;
if (!wallet.master.encrypted && (help || args.length !== 1))
throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"');
const valid = new Validator(args);
const passphrase = valid.str(0, '');
if (wallet.master.encrypted) {
throw new RPCError(errs.WALLET_WRONG_ENC_STATE,
'Already running with an encrypted wallet.');
}
if (passphrase.length < 1)
throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"');
try {
await wallet.encrypt(passphrase);
} catch (e) {
throw new RPCError(errs.WALLET_ENCRYPTION_FAILED, 'Encryption failed.');
}
return 'wallet encrypted; we do not need to stop!';
}
async getAccountAddress(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'getaccountaddress "account"');
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0, '');
if (!name)
name = 'default';
const addr = await wallet.receiveAddress(name);
if (!addr)
return '';
return addr.toString(this.network);
}
async getAccount(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'getaccount "bitcoinaddress"');
const wallet = this.wallet;
const valid = new Validator(args);
const addr = valid.str(0, '');
const hash = parseHash(addr, this.network);
const path = await wallet.getPath(hash);
if (!path)
return '';
return path.name;
}
async getAddressesByAccount(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'getaddressesbyaccount "account"');
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0, '');
const addrs = [];
if (name === '')
name = 'default';
let paths;
try {
paths = await wallet.getPaths(name);
} catch (e) {
if (e.message === 'Account not found.')
return [];
throw e;
}
for (const path of paths) {
const addr = path.toAddress();
addrs.push(addr.toString(this.network));
}
return addrs;
}
async getAddressInfo(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'getaddressinfo "address"');
const valid = new Validator(args);
const addr = valid.str(0, '');
const address = parseAddress(addr, this.network);
const script = Script.fromAddress(address);
const wallet = this.wallet.toJSON();
const path = await this.wallet.getPath(address);
const isScript = script.isScripthash() || script.isWitnessScripthash();
const isWitness = address.isProgram();
const result = {
address: address.toString(this.network),
scriptPubKey: script ? script.toJSON() : undefined,
ismine: path != null,
ischange: path ? path.branch === 1 : false,
iswatchonly: wallet.watchOnly,
isscript: isScript,
iswitness: isWitness
};
if (isWitness) {
result.witness_version = address.version;
result.witness_program = address.hash.toString('hex');
}
return result;
}
async getBalance(args, help) {
if (help || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'getbalance ( "account" minconf includeWatchonly )');
}
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0);
const minconf = valid.u32(1, 1);
const watchOnly = valid.bool(2, false);
if (name === '')
name = 'default';
if (name === '*')
name = null;
if (wallet.watchOnly !== watchOnly)
return 0;
const balance = await wallet.getBalance(name);
let value;
if (minconf > 0)
value = balance.confirmed;
else
value = balance.unconfirmed;
return Amount.btc(value, true);
}
async getNewAddress(args, help) {
if (help || args.length > 1)
throw new RPCError(errs.MISC_ERROR, 'getnewaddress ( "account" )');
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0);
if (name === '' || args.length === 0)
name = 'default';
const addr = await wallet.createReceive(name);
return addr.getAddress('string', this.network);
}
async getRawChangeAddress(args, help) {
if (help || args.length !== 0)
throw new RPCError(errs.MISC_ERROR, 'getrawchangeaddress');
const wallet = this.wallet;
const addr = await wallet.createChange();
return addr.getAddress('string', this.network);
}
async getReceivedByAccount(args, help) {
if (help || args.length < 1 || args.length > 2) {
throw new RPCError(errs.MISC_ERROR,
'getreceivedbyaccount "account" ( minconf )');
}
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0);
const minconf = valid.u32(1, 0);
const height = this.wdb.state.height;
if (name === '')
name = 'default';
const paths = await wallet.getPaths(name);
const filter = new BufferSet();
for (const path of paths)
filter.add(path.hash);
const txs = await wallet.getHistory(name);
let total = 0;
let lastConf = -1;
for (const wtx of txs) {
const conf = wtx.getDepth(height);
if (conf < minconf)
continue;
if (lastConf === -1 || conf < lastConf)
lastConf = conf;
for (const output of wtx.tx.outputs) {
const hash = output.getHash();
if (hash && filter.has(hash))
total += output.value;
}
}
return Amount.btc(total, true);
}
async getReceivedByAddress(args, help) {
if (help || args.length < 1 || args.length > 2) {
throw new RPCError(errs.MISC_ERROR,
'getreceivedbyaddress "bitcoinaddress" ( minconf )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const addr = valid.str(0, '');
const minconf = valid.u32(1, 0);
const height = this.wdb.state.height;
const hash = parseHash(addr, this.network);
const txs = await wallet.getHistory();
let total = 0;
for (const wtx of txs) {
if (wtx.getDepth(height) < minconf)
continue;
for (const output of wtx.tx.outputs) {
if (output.getHash().equals(hash))
total += output.value;
}
}
return Amount.btc(total, true);
}
async _toWalletTX(wtx) {
const wallet = this.wallet;
const details = await wallet.toDetails(wtx);
if (!details)
throw new RPCError(errs.WALLET_ERROR, 'TX not found.');
let receive = true;
for (const member of details.inputs) {
if (member.path) {
receive = false;
break;
}
}
const det = [];
let sent = 0;
let received = 0;
for (let i = 0; i < details.outputs.length; i++) {
const member = details.outputs[i];
if (member.path) {
if (member.path.branch === 1)
continue;
det.push({
account: member.path.name,
address: member.address.toString(this.network),
category: 'receive',
amount: Amount.btc(member.value, true),
label: member.path.name,
vout: i
});
received += member.value;
continue;
}
if (receive)
continue;
det.push({
account: '',
address: member.address
? member.address.toString(this.network)
: null,
category: 'send',
amount: -(Amount.btc(member.value, true)),
fee: -(Amount.btc(details.fee, true)),
vout: i
});
sent += member.value;
}
return {
amount: Amount.btc(receive ? received : -sent, true),
confirmations: details.confirmations,
blockhash: details.block ? util.revHex(details.block) : null,
blockindex: details.index,
blocktime: details.time,
txid: util.revHex(details.hash),
walletconflicts: [],
time: details.mtime,
timereceived: details.mtime,
'bip125-replaceable': 'no',
details: det,
hex: details.tx.toRaw().toString('hex')
};
}
async getTransaction(args, help) {
if (help || args.length < 1 || args.length > 2) {
throw new RPCError(errs.MISC_ERROR,
'gettransaction "txid" ( includeWatchonly )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const hash = valid.brhash(0);
const watchOnly = valid.bool(1, false);
if (!hash)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter');
const wtx = await wallet.getTX(hash);
if (!wtx)
throw new RPCError(errs.WALLET_ERROR, 'TX not found.');
return await this._toWalletTX(wtx, watchOnly);
}
async abandonTransaction(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'abandontransaction "txid"');
const wallet = this.wallet;
const valid = new Validator(args);
const hash = valid.brhash(0);
if (!hash)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
const result = await wallet.abandon(hash);
if (!result)
throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.');
return null;
}
async getUnconfirmedBalance(args, help) {
if (help || args.length > 0)
throw new RPCError(errs.MISC_ERROR, 'getunconfirmedbalance');
const wallet = this.wallet;
const balance = await wallet.getBalance();
return Amount.btc(balance.unconfirmed, true);
}
async getWalletInfo(args, help) {
if (help || args.length !== 0)
throw new RPCError(errs.MISC_ERROR, 'getwalletinfo');
const wallet = this.wallet;
const balance = await wallet.getBalance();
return {
walletid: wallet.id,
walletversion: 6,
balance: Amount.btc(balance.unconfirmed, true),
unconfirmed_balance: Amount.btc(balance.unconfirmed, true),
txcount: balance.tx,
keypoololdest: 0,
keypoolsize: 0,
unlocked_until: wallet.master.until,
paytxfee: Amount.btc(this.wdb.feeRate, true)
};
}
async importPrivKey(args, help) {
if (help || args.length < 1 || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'importprivkey "bitcoinprivkey" ( "label" rescan )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const secret = valid.str(0);
const rescan = valid.bool(2, false);
const key = parseSecret(secret, this.network);
await wallet.importKey(0, key);
if (rescan)
await this.wdb.rescan(0);
return null;
}
async importWallet(args, help) {
if (help || args.length < 1 || args.length > 2)
throw new RPCError(errs.MISC_ERROR, 'importwallet "filename" ( rescan )');
const wallet = this.wallet;
const valid = new Validator(args);
const file = valid.str(0);
const rescan = valid.bool(1, false);
if (fs.unsupported)
throw new RPCError(errs.INTERNAL_ERROR, 'FS not available.');
let data;
try {
data = await fs.readFile(file, 'utf8');
} catch (e) {
throw new RPCError(errs.INTERNAL_ERROR, e.code || '');
}
const lines = data.split(/\n+/);
const keys = [];
for (let line of lines) {
line = line.trim();
if (line.length === 0)
continue;
if (/^\s*#/.test(line))
continue;
const parts = line.split(/\s+/);
if (parts.length < 4)
throw new RPCError(errs.DESERIALIZATION_ERROR, 'Malformed wallet.');
const secret = parseSecret(parts[0], this.network);
keys.push(secret);
}
for (const key of keys)
await wallet.importKey(0, key);
if (rescan)
await this.wdb.rescan(0);
return null;
}
async importAddress(args, help) {
if (help || args.length < 1 || args.length > 4) {
throw new RPCError(errs.MISC_ERROR,
'importaddress "address" ( "label" rescan p2sh )');
}
const wallet = this.wallet;
const valid = new Validator(args);
let addr = valid.str(0, '');
const rescan = valid.bool(2, false);
const p2sh = valid.bool(3, false);
if (p2sh) {
let script = valid.buf(0);
if (!script)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.');
script = Script.fromRaw(script);
script = Script.fromScripthash(script.hash160());
addr = script.getAddress();
} else {
addr = parseAddress(addr, this.network);
}
try {
await wallet.importAddress(0, addr);
} catch (e) {
if (e.message !== 'Address already exists.')
throw e;
}
if (rescan)
await this.wdb.rescan(0);
return null;
}
async importPubkey(args, help) {
if (help || args.length < 1 || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'importpubkey "pubkey" ( "label" rescan )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const data = valid.buf(0);
const rescan = valid.bool(2, false);
if (!data)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
const key = KeyRing.fromPublic(data, this.network);
await wallet.importKey(0, key);
if (rescan)
await this.wdb.rescan(0);
return null;
}
async keyPoolRefill(args, help) {
if (help || args.length > 1)
throw new RPCError(errs.MISC_ERROR, 'keypoolrefill ( newsize )');
return null;
}
async listAccounts(args, help) {
if (help || args.length > 2) {
throw new RPCError(errs.MISC_ERROR,
'listaccounts ( minconf includeWatchonly)');
}
const wallet = this.wallet;
const valid = new Validator(args);
const minconf = valid.u32(0, 0);
const watchOnly = valid.bool(1, false);
const accounts = await wallet.getAccounts();
const map = {};
for (const account of accounts) {
const balance = await wallet.getBalance(account);
let value = balance.unconfirmed;
if (minconf > 0)
value = balance.confirmed;
if (wallet.watchOnly !== watchOnly)
value = 0;
map[account] = Amount.btc(value, true);
}
return map;
}
async listAddressGroupings(args, help) {
throw new Error('Not implemented.');
}
async listLockUnspent(args, help) {
if (help || args.length > 0)
throw new RPCError(errs.MISC_ERROR, 'listlockunspent');
const wallet = this.wallet;
const outpoints = wallet.getLocked();
const out = [];
for (const outpoint of outpoints) {
out.push({
txid: outpoint.txid(),
vout: outpoint.index
});
}
return out;
}
async listReceivedByAccount(args, help) {
if (help || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'listreceivedbyaccount ( minconf includeempty includeWatchonly )');
}
const valid = new Validator(args);
const minconf = valid.u32(0, 0);
const includeEmpty = valid.bool(1, false);
const watchOnly = valid.bool(2, false);
return await this._listReceived(minconf, includeEmpty, watchOnly, true);
}
async listReceivedByAddress(args, help) {
if (help || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'listreceivedbyaddress ( minconf includeempty includeWatchonly )');
}
const valid = new Validator(args);
const minconf = valid.u32(0, 0);
const includeEmpty = valid.bool(1, false);
const watchOnly = valid.bool(2, false);
return await this._listReceived(minconf, includeEmpty, watchOnly, false);
}
async _listReceived(minconf, empty, watchOnly, account) {
const wallet = this.wallet;
const paths = await wallet.getPaths();
const height = this.wdb.state.height;
const map = new BufferMap();
for (const path of paths) {
const addr = path.toAddress();
map.set(path.hash, {
involvesWatchonly: wallet.watchOnly,
address: addr.toString(this.network),
account: path.name,
amount: 0,
confirmations: -1,
label: ''
});
}
const txs = await wallet.getHistory();
for (const wtx of txs) {
const conf = wtx.getDepth(height);
if (conf < minconf)
continue;
for (const output of wtx.tx.outputs) {
const addr = output.getAddress();
if (!addr)
continue;
const hash = addr.getHash();
const entry = map.get(hash);
if (entry) {
if (entry.confirmations === -1 || conf < entry.confirmations)
entry.confirmations = conf;
entry.address = addr.toString(this.network);
entry.amount += output.value;
}
}
}
let out = [];
for (const entry of map.values())
out.push(entry);
if (account) {
const map = new Map();
for (const entry of out) {
const item = map.get(entry.account);
if (!item) {
map.set(entry.account, entry);
entry.address = undefined;
continue;
}
item.amount += entry.amount;
}
out = [];
for (const entry of map.values())
out.push(entry);
}
const result = [];
for (const entry of out) {
if (!empty && entry.amount === 0)
continue;
if (entry.confirmations === -1)
entry.confirmations = 0;
entry.amount = Amount.btc(entry.amount, true);
result.push(entry);
}
return result;
}
async listSinceBlock(args, help) {
if (help || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'listsinceblock ( "blockhash" target-confirmations includeWatchonly)');
}
const wallet = this.wallet;
const chainHeight = this.wdb.state.height;
const valid = new Validator(args);
const block = valid.brhash(0);
const minconf = valid.u32(1, 0);
const watchOnly = valid.bool(2, false);
if (wallet.watchOnly !== watchOnly)
return [];
let height = -1;
if (block) {
const entry = await this.client.getEntry(block);
if (entry)
height = entry.height;
else
throw new RPCError(errs.MISC_ERROR, 'Block not found.');
}
if (height === -1)
height = chainHeight;
const txs = await wallet.getHistory();
const out = [];
let highest = null;
for (const wtx of txs) {
if (wtx.height < height)
continue;
if (wtx.getDepth(chainHeight) < minconf)
continue;
if (!highest || wtx.height > highest)
highest = wtx;
const json = await this._toListTX(wtx);
out.push(json);
}
return {
transactions: out,
lastblock: highest && highest.block
? util.revHex(highest.block)
: util.revHex(consensus.ZERO_HASH)
};
}
async _toListTX(wtx) {
const wallet = this.wallet;
const details = await wallet.toDetails(wtx);
if (!details)
throw new RPCError(errs.WALLET_ERROR, 'TX not found.');
let receive = true;
for (const member of details.inputs) {
if (member.path) {
receive = false;
break;
}
}
let sent = 0;
let received = 0;
let sendMember = null;
let recMember = null;
let sendIndex = -1;
let recIndex = -1;
for (let i = 0; i < details.outputs.length; i++) {
const member = details.outputs[i];
if (member.path) {
if (member.path.branch === 1)
continue;
received += member.value;
recMember = member;
recIndex = i;
continue;
}
sent += member.value;
sendMember = member;
sendIndex = i;
}
let member = null;
let index = -1;
if (receive) {
assert(recMember);
member = recMember;
index = recIndex;
} else {
if (sendMember) {
member = sendMember;
index = sendIndex;
} else {
// In the odd case where we send to ourselves.
receive = true;
received = 0;
member = recMember;
index = recIndex;
}
}
let rbf = false;
if (wtx.height === -1 && wtx.tx.isRBF())
rbf = true;
return {
account: member.path ? member.path.name : '',
address: member.address
? member.address.toString(this.network)
: null,
category: receive ? 'receive' : 'send',
amount: Amount.btc(receive ? received : -sent, true),
label: member.path ? member.path.name : undefined,
vout: index,
confirmations: details.getDepth(this.wdb.height),
blockhash: details.block ? util.revHex(details.block) : null,
blockindex: -1,
blocktime: details.time,
blockheight: details.height,
txid: util.revHex(details.hash),
walletconflicts: [],
time: details.mtime,
timereceived: details.mtime,
'bip125-replaceable': rbf ? 'yes' : 'no'
};
}
async listTransactions(args, help) {
if (help || args.length > 4) {
throw new RPCError(errs.MISC_ERROR,
'listtransactions ( "account" count from includeWatchonly)');
}
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0);
const count = valid.u32(1, 10);
const from = valid.u32(2, 0);
const watchOnly = valid.bool(3, false);
if (wallet.watchOnly !== watchOnly)
return [];
if (name === '')
name = 'default';
const txs = await wallet.getHistory(name);
common.sortTX(txs);
const end = from + count;
const to = Math.min(end, txs.length);
const out = [];
for (let i = from; i < to; i++) {
const wtx = txs[i];
const json = await this._toListTX(wtx);
out.push(json);
}
return out;
}
async listUnspent(args, help) {
if (help || args.length > 3) {
throw new RPCError(errs.MISC_ERROR,
'listunspent ( minconf maxconf ["address",...] )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const minDepth = valid.u32(0, 1);
const maxDepth = valid.u32(1, 9999999);
const addrs = valid.array(2);
const height = this.wdb.state.height;
const map = new BufferSet();
if (addrs) {
const valid = new Validator(addrs);
for (let i = 0; i < addrs.length; i++) {
const addr = valid.str(i, '');
const hash = parseHash(addr, this.network);
if (map.has(hash))
throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address.');
map.add(hash);
}
}
const coins = await wallet.getCoins();
common.sortCoins(coins);
const out = [];
for (const coin of coins) {
const depth = coin.getDepth(height);
if (depth < minDepth || depth > maxDepth)
continue;
const addr = coin.getAddress();
if (!addr)
continue;
const hash = coin.getHash();
if (addrs) {
if (!hash || !map.has(hash))
continue;
}
const ring = await wallet.getKey(hash);
out.push({
txid: coin.txid(),
vout: coin.index,
address: addr ? addr.toString(this.network) : null,
account: ring ? ring.name : undefined,
redeemScript: ring && ring.script
? ring.script.toJSON()
: undefined,
scriptPubKey: coin.script.toJSON(),
amount: Amount.btc(coin.value, true),
confirmations: depth,
spendable: !wallet.isLocked(coin),
solvable: true
});
}
return out;
}
async lockUnspent(args, help) {
if (help || args.length < 1 || args.length > 2) {
throw new RPCError(errs.MISC_ERROR,
'lockunspent unlock ([{"txid":"txid","vout":n},...])');
}
const wallet = this.wallet;
const valid = new Validator(args);
const unlock = valid.bool(0, false);
const outputs = valid.array(1);
if (args.length === 1) {
if (unlock)
wallet.unlockCoins();
return true;
}
if (!outputs)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
for (const output of outputs) {
const valid = new Validator(output);
const hash = valid.brhash('txid');
const index = valid.u32('vout');
if (hash == null || index == null)
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.');
const outpoint = new Outpoint(hash, index);
if (unlock) {
wallet.unlockCoin(outpoint);
continue;
}
wallet.lockCoin(outpoint);
}
return true;
}
async move(args, help) {
// Not implementing: stupid and deprecated.
throw new Error('Not implemented.');
}
async sendFrom(args, help) {
if (help || args.length < 3 || args.length > 6) {
throw new RPCError(errs.MISC_ERROR,
'sendfrom "fromaccount" "tobitcoinaddress"'
+ ' amount ( minconf "comment" "comment-to" )');
}
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0);
const str = valid.str(1);
const value = valid.ufixed(2, 8);
const minconf = valid.u32(3, 0);
const addr = parseAddress(str, this.network);
if (!addr || value == null)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
if (name === '')
name = 'default';
const options = {
account: name,
depth: minconf,
outputs: [{
address: addr,
value: value
}]
};
const tx = await wallet.send(options);
return tx.txid();
}
async sendMany(args, help) {
if (help || args.length < 2 || args.length > 5) {
throw new RPCError(errs.MISC_ERROR,
'sendmany "fromaccount" {"address":amount,...}'
+ ' ( minconf "comment" subtractfee )');
}
const wallet = this.wallet;
const valid = new Validator(args);
let name = valid.str(0);
const sendTo = valid.obj(1);
const minconf = valid.u32(2, 1);
const subtract = valid.bool(4, false);
if (name === '')
name = 'default';
if (!sendTo)
throw new RPCError(errs.TYPE_ERROR, 'Invalid send-to address.');
const to = new Validator(sendTo);
const uniq = new BufferSet();
const outputs = [];
for (const key of Object.keys(sendTo)) {
const value = to.ufixed(key, 8);
const addr = parseAddress(key, this.network);
const hash = addr.getHash();
if (value == null)
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid amount.');
if (uniq.has(hash))
throw new RPCError(errs.INVALID_PARAMETER,
'Each send-to address must be unique.');
uniq.add(hash);
const output = new Output();
output.value = value;
output.script.fromAddress(addr);
outputs.push(output);
}
const options = {
outputs: outputs,
subtractFee: subtract,
account: name,
depth: minconf
};
const tx = await wallet.send(options);
return tx.txid();
}
async sendToAddress(args, help) {
if (help || args.length < 2 || args.length > 5) {
throw new RPCError(errs.MISC_ERROR,
'sendtoaddress "bitcoinaddress" amount'
+ ' ( "comment" "comment-to" subtractfeefromamount )');
}
const wallet = this.wallet;
const valid = new Validator(args);
const str = valid.str(0);
const value = valid.ufixed(1, 8);
const subtract = valid.bool(4, false);
const addr = parseAddress(str, this.network);
if (!addr || value == null)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
const options = {
subtractFee: subtract,
outputs: [{
address: addr,
value: value
}]
};
const tx = await wallet.send(options);
return tx.txid();
}
async setAccount(args, help) {
// Impossible to implement in bcoin:
throw new Error('Not implemented.');
}
async setTXFee(args, help) {
const valid = new Validator(args);
const rate = valid.ufixed(0, 8);
if (help || args.length < 1 || args.length > 1)
throw new RPCError(errs.MISC_ERROR, 'settxfee amount');
if (rate == null)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
this.wdb.feeRate = rate;
return true;
}
async signMessage(args, help) {
if (help || args.length !== 2) {
throw new RPCError(errs.MISC_ERROR,
'signmessage "bitcoinaddress" "message"');
}
const wallet = this.wallet;
const valid = new Validator(args);
const b58 = valid.str(0, '');
const str = valid.str(1, '');
const addr = parseHash(b58, this.network);
const ring = await wallet.getKey(addr);
if (!ring)
throw new RPCError(errs.WALLET_ERROR, 'Address not found.');
if (!wallet.master.key)
throw new RPCError(errs.WALLET_UNLOCK_NEEDED, 'Wallet is locked.');
const sig = messageUtil.sign(str, ring);
return sig.toString('base64');
}
async walletLock(args, help) {
const wallet = this.wallet;
if (help || (wallet.master.encrypted && args.length !== 0))
throw new RPCError(errs.MISC_ERROR, 'walletlock');
if (!wallet.master.encrypted) {
throw new RPCError(
errs.WALLET_WRONG_ENC_STATE,
'Wallet is not encrypted.');
}
await wallet.lock();
return null;
}
async walletPassphraseChange(args, help) {
const wallet = this.wallet;
if (help || (wallet.master.encrypted && args.length !== 2)) {
throw new RPCError(errs.MISC_ERROR, 'walletpassphrasechange'
+ ' "oldpassphrase" "newpassphrase"');
}
const valid = new Validator(args);
const old = valid.str(0, '');
const passphrase = valid.str(1, '');
if (!wallet.master.encrypted) {
throw new RPCError(
errs.WALLET_WRONG_ENC_STATE,
'Wallet is not encrypted.');
}
if (old.length < 1 || passphrase.length < 1)
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter');
await wallet.setPassphrase(passphrase, old);
return null;
}
async walletPassphrase(args, help) {
const wallet = this.wallet;
const valid = new Validator(args);
const passphrase = valid.str(0, '');
const timeout = valid.u32(1);
if (help || (wallet.master.encrypted && args.length !== 2)) {
throw new RPCError(errs.MISC_ERROR,
'walletpassphrase "passphrase" timeout');
}
if (!wallet.master.encrypted) {
throw new RPCError(
errs.WALLET_WRONG_ENC_STATE,
'Wallet is not encrypted.');
}
if (passphrase.length < 1)
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter');
if (timeout == null)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter');
await wallet.unlock(passphrase, timeout);
return null;
}
async importPrunedFunds(args, help) {
if (help || args.length !== 2) {
throw new RPCError(errs.MISC_ERROR,
'importprunedfunds "rawtransaction" "txoutproof"');
}
const valid = new Validator(args);
const txRaw = valid.buf(0);
const blockRaw = valid.buf(1);
if (!txRaw || !blockRaw)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
const tx = TX.fromRaw(txRaw);
const block = MerkleBlock.fromRaw(blockRaw);
const hash = block.hash();
if (!block.verify())
throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.');
if (!block.hasTX(tx.hash()))
throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.');
const entry = await this.client.getEntry(hash);
if (!entry)
throw new RPCError(errs.VERIFY_ERROR, 'Invalid proof.');
const meta = BlockMeta.fromEntry(entry);
if (!await this.wdb.addTX(tx, meta))
throw new RPCError(errs.WALLET_ERROR,
'Address for TX not in wallet, or TX already in wallet');
return null;
}
async removePrunedFunds(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'removeprunedfunds "txid"');
const wallet = this.wallet;
const valid = new Validator(args);
const hash = valid.brhash(0);
if (!hash)
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.');
if (!await wallet.remove(hash))
throw new RPCError(errs.WALLET_ERROR, 'Transaction not in wallet.');
return null;
}
async selectWallet(args, help) {
const valid = new Validator(args);
const id = valid.str(0);
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'selectwallet "id"');
const wallet = await this.wdb.get(id);
if (!wallet)
throw new RPCError(errs.WALLET_ERROR, 'Wallet not found.');
this.wallet = wallet;
return null;
}
async getMemoryInfo(args, help) {
if (help || args.length !== 0)
throw new RPCError(errs.MISC_ERROR, 'getmemoryinfo');
return this.logger.memoryUsage();
}
async setLogLevel(args, help) {
if (help || args.length !== 1)
throw new RPCError(errs.MISC_ERROR, 'setloglevel "level"');
const valid = new Validator(args);
const level = valid.str(0, '');
this.logger.setLevel(level);
return null;
}
}
/*
* Helpers
*/
function parseHash(raw, network) {
const addr = parseAddress(raw, network);
return addr.getHash();
}
function parseAddress(raw, network) {
try {
return Address.fromString(raw, network);
} catch (e) {
throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid address.');
}
}
function parseSecret(raw, network) {
try {
return KeyRing.fromSecret(raw, network);
} catch (e) {
throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid key.');
}
}
/*
* Expose
*/
module.exports = RPC;