UNPKG

hsd

Version:
1,893 lines (1,478 loc) 77.5 kB
/*! * rpc.js - bitcoind-compatible json rpc for hsd. * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const {format} = require('util'); const bweb = require('bweb'); const {Lock} = require('bmutex'); const fs = require('bfile'); const {BufferMap, BufferSet} = require('buffer-map'); const Validator = require('bval'); const blake2b = require('bcrypto/lib/blake2b'); const util = require('../utils/util'); const Amount = require('../ui/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 rules = require('../covenants/rules'); const {Resource} = require('../dns/resource'); const {EXP} = consensus; const RPCBase = bweb.RPC; const RPCError = bweb.RPCError; /** @typedef {import('./node')} WalletNode */ /** @typedef {import('./plugin').Plugin} Plugin */ /* * 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 }; const MAGIC_STRING = `${pkg.currency} signed message:\n`; /** * Wallet RPC * @alias module:wallet.RPC */ class RPC extends RPCBase { /** * Create an RPC. * @param {WalletNode|Plugin} node */ 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.maxTXs = this.wdb.options.maxHistoryTXs; 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); } handleError(err) { this.logger.error('RPC internal error.'); this.logger.error(err); } 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('backupwallet', this.backupWallet); this.add('dumpprivkey', this.dumpPrivKey); this.add('dumpwallet', this.dumpWallet); this.add('encryptwallet', this.encryptWallet); this.add('getaccountaddress', this.getAccountAddress); this.add('getaccount', this.getAccount); this.add('getaddressesbyaccount', this.getAddressesByAccount); this.add('getaddressinfo', this.getAddressInfo); 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('importname', this.importName); 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('listhistory', this.listHistory); this.add('listhistoryafter', this.listHistoryAfter); this.add('listhistorybytime', this.listHistoryByTime); this.add('listunconfirmed', this.listUnconfirmed); this.add('listunconfirmedafter', this.listUnconfirmedAfter); this.add('listunconfirmedbytime', this.listUnconfirmedByTime); this.add('listunspent', this.listUnspent); this.add('lockunspent', this.lockUnspent); this.add('sendfrom', this.sendFrom); this.add('sendmany', this.sendMany); this.add('sendtoaddress', this.sendToAddress); this.add('createsendtoaddress', this.createSendToAddress); this.add('setaccount', this.setAccount); this.add('settxfee', this.setTXFee); this.add('signmessage', this.signMessage); this.add('signmessagewithname', this.signMessageWithName); 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); this.add('getbids', this.getBids); this.add('getreveals', this.getReveals); this.add('getnames', this.getNames); this.add('getauctioninfo', this.getAuctionInfo); this.add('getnameinfo', this.getNameInfo); this.add('getnameresource', this.getNameResource); this.add('getnamebyhash', this.getNameByHash); this.add('createclaim', this.createClaim); this.add('sendfakeclaim', this.sendFakeClaim); this.add('sendclaim', this.sendClaim); this.add('sendopen', this.sendOpen); this.add('sendbid', this.sendBid); this.add('sendreveal', this.sendReveal); this.add('sendredeem', this.sendRedeem); this.add('sendupdate', this.sendUpdate); this.add('sendrenewal', this.sendRenewal); this.add('sendtransfer', this.sendTransfer); this.add('sendcancel', this.sendCancel); this.add('sendfinalize', this.sendFinalize); this.add('sendrevoke', this.sendRevoke); this.add('sendbatch', this.sendBatch); this.add('importnonce', this.importNonce); this.add('createopen', this.createOpen); this.add('createbid', this.createBid); this.add('createreveal', this.createReveal); this.add('createredeem', this.createRedeem); this.add('createupdate', this.createUpdate); this.add('createrenewal', this.createRenewal); this.add('createtransfer', this.createTransfer); this.add('createcancel', this.createCancel); this.add('createfinalize', this.createFinalize); this.add('createrevoke', this.createRevoke); this.add('createbatch', this.createBatch); // Compat this.add('getauctions', this.getNames); // this.add('getauctioninfo', this.getAuctionInfo); // this.add('getnameinfo', this.getNameInfo); // this.add('getnameresource', this.getNameResource); } async help(args, _help) { if (args.length === 0) return 'Select a command.'; 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.decode(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', EXP); change = valid.str('changeAddress'); if (change) change = parseAddress(change, this.network); } await wallet.fund(tx, { rate: rate, changeAddress: change }); return { hex: tx.toHex(), changepos: tx.changeIndex, fee: Amount.coin(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 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 "address"'); 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 ${pkg.name} %s`, pkg.version), format('# * Created on %s', time), format('# * Best block at time of backup was %d (%s).', tip.height, tip.hash.toString('hex')), 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().toString(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 "address"'); 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'; const paths = await wallet.getPaths(name); 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 wallet = this.wallet.toJSON(); const path = await this.wallet.getPath(address); return { address: address.toString(this.network), ismine: path != null, iswatchonly: wallet.watchOnly, ischange: path ? path.branch === 1 : false, isspendable: !address.isUnspendable(), isscript: address.isScripthash(), witness_version: address.version, witness_program: address.hash.toString('hex') }; } 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.coin(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 === '') name = 'default'; const addr = await wallet.createReceive(name); return addr.getAddress().toString(this.network); } async getRawChangeAddress(args, help) { if (help || args.length > 1) throw new RPCError(errs.MISC_ERROR, 'getrawchangeaddress'); const wallet = this.wallet; const addr = await wallet.createChange(); return addr.getAddress().toString(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); let txs = await wallet.listHistory(name, { limit: this.maxTXs, reverse: false }); let total = 0; let lastConf = -1; // While this doesn't consume a lot of memory // it can still consume a lot of CPU and be very // slow. If this is a common information to query, // it would be better to calculate the total per // account while indexing and adding blocks. while (txs.length) { 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; } } txs = await wallet.listHistoryAfter(name, { hash: txs[txs.length - 1].hash, limit: this.maxTXs, reverse: false }); } return Amount.coin(total, true); } async getReceivedByAddress(args, help) { if (help || args.length < 1 || args.length > 2) { throw new RPCError(errs.MISC_ERROR, 'getreceivedbyaddress "address" ( 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); // While this doesn't consume a lot of memory // it can still consume a lot of CPU and be very // slow. If this is a common information to query, // it would be better to calculate the total per // address while indexing and adding blocks. let txs = await wallet.listHistory(null, { limit: this.maxTXs, reverse: false }); let total = 0; while (txs.length) { for (const wtx of txs) { if (wtx.getDepth(height) < minconf) continue; for (const output of wtx.tx.outputs) { const ohash = output.getHash(); if (ohash && ohash.equals(hash)) total += output.value; } } txs = await wallet.listHistoryAfter(null, { hash: txs[txs.length - 1].hash, limit: this.maxTXs, reverse: false }); } return Amount.coin(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 = []; const fee = details.getFee(); 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.coin(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.coin(member.value, true)), fee: -(Amount.coin(fee, true)), vout: i }); sent += member.value; } return { amount: Amount.coin(receive ? received : -sent, true), confirmations: details.confirmations, blockhash: details.block ? details.block.toString('hex') : null, blockindex: details.index, blocktime: details.time, txid: details.hash.toString('hex'), walletconflicts: [], time: details.mtime, timereceived: details.mtime, 'bip125-replaceable': 'no', details: det, hex: details.tx.toHex() }; } 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.bhash(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.bhash(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.coin(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.coin(balance.unconfirmed, true), unconfirmed_balance: Amount.coin(balance.unconfirmed, true), txcount: balance.tx, keypoololdest: 0, keypoolsize: 0, unlocked_until: wallet.master.until, paytxfee: Amount.coin(this.wdb.feeRate, true), height: this.wdb.height }; } async importPrivKey(args, help) { if (help || args.length < 1 || args.length > 3) { throw new RPCError(errs.MISC_ERROR, 'importprivkey "privkey" ( "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.decode(script); addr = Address.fromScripthash(script.sha3()); } else { addr = parseAddress(addr, this.network); } await wallet.importAddress(0, addr); 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 importName(args, help) { if (help || args.length < 1 || args.length > 2) { throw new RPCError(errs.MISC_ERROR, 'importname "name" ( height )'); } const wallet = this.wallet; const valid = new Validator(args); const name = valid.str(0); const height = valid.u32(1); if (!name || !rules.verifyName(name)) throw new RPCError(errs.TYPE_ERROR, 'Invalid name.'); await wallet.importName(name); if (height != null) await this.wdb.rescan(height); 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 = Object.create(null); 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.coin(value, true); } return map; } async listAddressGroupings(args, help) { if (help) throw new RPCError(errs.MISC_ERROR, 'listaddressgroupings'); 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: '' }); } // With large number of paths this could consume a lot // of memory and give back large results. There is also // the potential for the query to have a large CPU hit // and be slow. If this is a common to query, it would // be better to calculate while indexing and adding // blocks instead of at query time. let txs = await wallet.listHistory(null, { limit: this.maxTXs, reverse: false }); while (txs.length) { 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; } } } txs = await wallet.listHistoryAfter(null, { hash: txs[txs.length -1].hash, limit: this.maxTXs, reverse: false }); } 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.coin(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.bhash(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) throw new RPCError(errs.MISC_ERROR, 'Block not found'); height = entry.height; } if (height === -1) height = chainHeight; let txs = await wallet.listHistory(null, { limit: this.maxTXs, reverse: false }); const out = []; let highest = null; while (txs.length) { 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); } txs = await wallet.listHistoryAfter(null, { hash: txs[txs.length - 1].hash, limit: this.maxTXs, reverse: false }); } return { transactions: out, lastblock: highest && highest.block ? highest.block.toString('hex') : consensus.ZERO_HASH.toString('hex') }; } 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; } } return { account: member.path ? member.path.name : '', address: member.address ? member.address.toString(this.network) : null, category: receive ? 'receive' : 'send', amount: Amount.coin(receive ? received : -sent, true), label: member.path ? member.path.name : undefined, vout: index, confirmations: details.getDepth(this.wdb.height), blockhash: details.block ? details.block.toString('hex') : null, blockindex: -1, blocktime: details.time, blockheight: details.height, txid: details.hash.toString('hex'), walletconflicts: [], time: details.mtime, timereceived: details.mtime }; } async listTransactions(args, help) { throw new Error('Deprecated: `listtransactions`. ' + 'Use `listhistory` and related methods.'); } async listHistory(args, help) { if (help || args.length > 4) { throw new RPCError(errs.MISC_ERROR, 'listhistory "account" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); const limit = valid.u32(1, this.maxTXs); const reverse = valid.bool(2, false); if (limit > this.maxTXs) { throw new RPCError(errs.INVALID_PARAMETER, `Limit above max of ${this.maxTXs}.`); } if (name === '*') name = null; const recs = await wallet.listHistory(name, {limit, reverse}); const out = []; for (let i = 0; i < recs.length; i++) out.push(await this._toListTX(recs[i])); return out; } async listHistoryAfter(args, help) { if (help || args.length > 4 || args.length < 2) { throw new RPCError(errs.MISC_ERROR, 'listhistoryafter "account", "txid" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); const hash = valid.bhash(1); const limit = valid.u32(2, this.maxTXs); const reverse = valid.bool(3, false); if (limit > this.maxTXs) { throw new RPCError(errs.INVALID_PARAMETER, `Limit above max of ${this.maxTXs}.`); } if (name === '*') name = null; const recs = await wallet.listHistoryAfter(name, { hash, limit, reverse }); const out = []; for (let i = 0; i < recs.length; i++) out.push(await this._toListTX(recs[i])); return out; } async listHistoryByTime(args, help) { if (help || args.length > 4 || args.length < 2) { throw new RPCError(errs.MISC_ERROR, 'listhistorybytime "account", "timestamp" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); const time = valid.uint(1); const limit = valid.u32(2, this.maxTXs); const reverse = valid.bool(3, false); if (limit > this.maxTXs) { throw new RPCError(errs.INVALID_PARAMETER, `Limit above max of ${this.maxTXs}.`); } if (name === '*') name = null; const recs = await wallet.listHistoryByTime(name, {time, limit, reverse}); const out = []; for (let i = 0; i < recs.length; i++) out.push(await this._toListTX(recs[i])); return out; } async listUnconfirmed(args, help) { if (help || args.length > 4) { throw new RPCError(errs.MISC_ERROR, 'listunconfirmed "account" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); const limit = valid.u32(1, this.maxTXs); const reverse = valid.bool(2, false); if (limit > this.maxTXs) { throw new RPCError(errs.INVALID_PARAMETER, `Limit above max of ${this.maxTXs}.`); } if (name === '*') name = null; const recs = await wallet.listUnconfirmed(name, {limit, reverse}); const out = []; for (let i = 0; i < recs.length; i++) out.push(await this._toListTX(recs[i])); return out; } async listUnconfirmedAfter(args, help) { if (help || args.length > 4 || args.length < 2) { throw new RPCError(errs.MISC_ERROR, 'listunconfirmedafter "account", "txid" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); const hash = valid.bhash(1); const limit = valid.u32(2, this.maxTXs); const reverse = valid.bool(3, false); if (!hash) throw new RPCError(errs.INVALID_PARAMETER, 'Missing txid parameter.'); if (limit > this.maxTXs) { throw new RPCError(errs.INVALID_PARAMETER, `Limit above max of ${this.maxTXs}.`); } if (name === '*') name = null; const recs = await wallet.listUnconfirmedAfter(name, { hash, limit, reverse }); const out = []; for (let i = 0; i < recs.length; i++) out.push(await this._toListTX(recs[i])); return out; } async listUnconfirmedByTime(args, help) { if (help || args.length > 4 || args.length < 2) { throw new RPCError(errs.MISC_ERROR, 'listunconfirmedbytime "account", "timestamp" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); const time = valid.uint(1); const limit = valid.u32(2, this.maxTXs); const reverse = valid.bool(3, false); if (limit > this.maxTXs) { throw new RPCError(errs.INVALID_PARAMETER, `Limit above max of ${this.maxTXs}.`); } if (name === '*') name = null; const recs = await wallet.listUnconfirmedByTime(name, { time, limit, reverse }); const out = []; for (let i = 0; i < recs.length; i++) out.push(await this._toListTX(recs[i])); 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 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: coin.address.toString(this.network), account: ring ? ring.name : undefined, redeemScript: ring && ring.script ? ring.script.toJSON() : undefined, amount: Amount.coin(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.bhash('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 sendFrom(args, help) { if (help || args.length < 3 || args.length > 6) { throw new RPCError(errs.MISC_ERROR, 'sendfrom "fromaccount" "toaddress"' + ' 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, EXP); 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" ["address",...] )'); } 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 parameter.'); const to = new Validator(sendTo); const uniq = new BufferSet(); const outputs = []; for (const key of Object.keys(sendTo)) { const value = to.ufixed(key, EXP); const addr = parseAddress(key, this.network); const hash = addr.getHash(); if (value == null) throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); if (uniq.has(hash)) throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); uniq.add(hash); const output = new Output(); output.value = value; output.address = 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) { const opts = this._validateSendToAddress(args, help, 'sendtoaddress'); const wallet = this.wallet; const options = { account: opts.account, subtractFee: opts.subtract, outputs: [{ address: opts.addr, value: opts.value }] }; const tx = await wallet.send(options); return tx.txid(); } async createSendToAddress(args, help) { const opts = this._validateSendToAddress(args, help, 'createsendtoaddress'); const wallet = this.wallet; const options = { paths: true, account: opts.account, subtractFee: opts.subtract, outputs: [{ address: opts.addr, value: opts.value }] }; const mtx = await wallet.createTX(options); return mtx.getJSON(this.network); } _validateSendToAddress(args, help, method) { const msg = `${method} "address" amount ` + '( "comment" "comment-to" subtractfeefromamount "account" )'; if (help || args.length < 2 || args.length > 6) throw new RPCError(errs.MISC_ERROR, msg); const valid = new Validator(args); const str = valid.str(0); const value = valid.ufixed(1, EXP); const subtract = valid.bool(4, false); const account = valid.str(5); const addr = parseAddress(str, this.network); if (!addr || value == null) throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); return { subtract, addr, value, account }; } async setAccount(args, help) { if (help || args.length < 1 || args.length > 2) { throw new RPCError(errs.MISC_ERROR, 'setaccount "address" "account"'); } // Impossible to implement: throw new Error('Not implemented.'); } async setTXFee(args, help) { const valid = new Validator(args); const rate = valid.ufixed(0, EXP); 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 "address" "message"'); } const wallet = this.wallet; const valid = new Validator(args); const b58 = valid.str(0, ''); const str = valid.str(1, ''); const addr = parseAddress(b58, this.network); if (!addr.isPubkeyhash()) { throw new RPCError( errs.INVALID_ADDRESS_OR_KEY, 'Version 0 pubkeyhash address required for signing.' ); } const ring = await wallet.getKey(addr.getHash()); 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 msg = Buffer.from(MAGIC_STRING + str, 'utf8'); const hash = blake2b.digest(msg); const sig = ring.sign(hash); return sig.toString('base64'); } async signMessageWithName(args, help) { if (help || args.length !== 2) { throw new RPCError(errs.MISC_ERROR, 'signmessagewithname "name" "message"'); } const wallet = this.wallet; const valid = new Validator(args); const name = valid.str(0, ''); const str = valid.str(1, ''); const height = this.wdb.height; const network = this.network; if (!name || !rules.verifyName(name)) throw new RPCError(errs.TYPE_ERROR, 'Invalid name.'); const ns = await wallet.getNameStateByName(name); if (!ns || !ns.owner) throw new RPCError(errs.MISC_ERROR, 'Cannot find the name owner.'); if (!ns.isClosed(height, network)) throw new Error('Invalid name state.'); const coin = await wallet.getCoin(ns.owner.hash, ns.owner.index); if (!coin) { throw new RPCError( errs.DATABASE_ERROR, 'Cannot find name owner\'s coin in wallet.' ); } const address = coin.address.toString(this.network); return this.signMessage([address, str], help); } 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