- Added totalSigs property to the JSON response object - Use getMultisig() function to return n value if available
3207 lines
82 KiB
JavaScript
3207 lines
82 KiB
JavaScript
/*!
|
|
* 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 bweb = require('bweb');
|
|
const {Lock} = require('bmutex');
|
|
const IP = require('binet');
|
|
const Validator = require('bval');
|
|
const {BufferMap, BufferSet} = require('buffer-map');
|
|
const blake2b = require('bcrypto/lib/blake2b');
|
|
const {safeEqual} = require('bcrypto/lib/safe');
|
|
const secp256k1 = require('bcrypto/lib/secp256k1');
|
|
const util = require('../utils/util');
|
|
const common = require('../blockchain/common');
|
|
const Amount = require('../ui/amount');
|
|
const NetAddress = require('../net/netaddress');
|
|
const Script = require('../script/script');
|
|
const Address = require('../primitives/address');
|
|
const Block = require('../primitives/block');
|
|
const Input = require('../primitives/input');
|
|
const KeyRing = require('../primitives/keyring');
|
|
const MerkleBlock = require('../primitives/merkleblock');
|
|
const Headers = require('../primitives/headers');
|
|
const MTX = require('../primitives/mtx');
|
|
const Network = require('../protocol/network');
|
|
const Outpoint = require('../primitives/outpoint');
|
|
const Output = require('../primitives/output');
|
|
const TX = require('../primitives/tx');
|
|
const Claim = require('../primitives/claim');
|
|
const consensus = require('../protocol/consensus');
|
|
const pkg = require('../pkg');
|
|
const rules = require('../covenants/rules');
|
|
const {Resource} = require('../dns/resource');
|
|
const NameState = require('../covenants/namestate');
|
|
const {ownership} = require('../covenants/ownership');
|
|
const AirdropProof = require('../primitives/airdropproof');
|
|
const {EXP} = consensus;
|
|
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,
|
|
|
|
// P2P client errors
|
|
CLIENT_NOT_CONNECTED: -9,
|
|
CLIENT_IN_INITIAL_DOWNLOAD: -10,
|
|
CLIENT_NODE_ALREADY_ADDED: -23,
|
|
CLIENT_NODE_NOT_ADDED: -24,
|
|
CLIENT_NODE_NOT_CONNECTED: -29,
|
|
CLIENT_INVALID_IP_OR_SUBNET: -30,
|
|
CLIENT_P2P_DISABLED: -31
|
|
};
|
|
|
|
const MAGIC_STRING = `${pkg.currency} signed message:\n`;
|
|
|
|
/**
|
|
* Handshake RPC
|
|
* @alias module:http.RPC
|
|
* @extends bweb.RPC
|
|
*/
|
|
|
|
class RPC extends RPCBase {
|
|
/**
|
|
* Create RPC.
|
|
* @param {Node} node
|
|
*/
|
|
|
|
constructor(node) {
|
|
super();
|
|
|
|
assert(node, 'RPC requires a Node.');
|
|
|
|
this.node = node;
|
|
this.network = node.network;
|
|
this.workers = node.workers;
|
|
this.chain = node.chain;
|
|
this.mempool = node.mempool;
|
|
this.pool = node.pool;
|
|
this.fees = node.fees;
|
|
this.miner = node.miner;
|
|
this.logger = node.logger.context('node-rpc');
|
|
this.locker = new Lock();
|
|
|
|
this.mining = false;
|
|
this.procLimit = 0;
|
|
this.attempt = null;
|
|
this.lastActivity = 0;
|
|
this.boundChain = false;
|
|
this.mask = Buffer.alloc(32, 0x00);
|
|
this.merkleMap = new BufferMap();
|
|
this.merkleList = [];
|
|
this.pollers = [];
|
|
|
|
this.init();
|
|
}
|
|
|
|
getCode(err) {
|
|
switch (err.type) {
|
|
case 'RPCError':
|
|
return err.code;
|
|
case 'ValidationError':
|
|
return errs.TYPE_ERROR;
|
|
case 'EncodingError':
|
|
return errs.DESERIALIZATION_ERROR;
|
|
default:
|
|
return errs.INTERNAL_ERROR;
|
|
}
|
|
}
|
|
|
|
handleCall(cmd, query) {
|
|
if (cmd.method !== 'getwork'
|
|
&& cmd.method !== 'getblocktemplate'
|
|
&& cmd.method !== 'getbestblockhash') {
|
|
this.logger.debug('Handling RPC call: %s.', cmd.method);
|
|
|
|
if (cmd.method !== 'submitblock')
|
|
this.logger.debug(cmd.params);
|
|
}
|
|
|
|
if (cmd.method === 'getwork') {
|
|
if (query.longpoll)
|
|
cmd.method = 'getworklp';
|
|
}
|
|
}
|
|
|
|
handleError(err) {
|
|
this.logger.error('RPC internal error.');
|
|
this.logger.error(err);
|
|
}
|
|
|
|
init() {
|
|
this.add('stop', this.stop);
|
|
this.add('help', this.help);
|
|
|
|
this.add('getblockchaininfo', this.getBlockchainInfo);
|
|
this.add('getbestblockhash', this.getBestBlockHash);
|
|
this.add('getblockcount', this.getBlockCount);
|
|
this.add('getblock', this.getBlock);
|
|
this.add('getblockbyheight', this.getBlockByHeight);
|
|
this.add('getblockhash', this.getBlockHash);
|
|
this.add('getblockheader', this.getBlockHeader);
|
|
this.add('getchaintips', this.getChainTips);
|
|
this.add('getdifficulty', this.getDifficulty);
|
|
this.add('getmempoolancestors', this.getMempoolAncestors);
|
|
this.add('getmempooldescendants', this.getMempoolDescendants);
|
|
this.add('getmempoolentry', this.getMempoolEntry);
|
|
this.add('getmempoolinfo', this.getMempoolInfo);
|
|
this.add('getrawmempool', this.getRawMempool);
|
|
this.add('gettxout', this.getTXOut);
|
|
this.add('gettxoutsetinfo', this.getTXOutSetInfo);
|
|
this.add('pruneblockchain', this.pruneBlockchain);
|
|
this.add('compacttree', this.compactTree);
|
|
this.add('reconstructtree', this.reconstructTree);
|
|
this.add('verifychain', this.verifyChain);
|
|
|
|
this.add('invalidateblock', this.invalidateBlock);
|
|
this.add('reconsiderblock', this.reconsiderBlock);
|
|
|
|
this.add('getnetworkhashps', this.getNetworkHashPS);
|
|
this.add('getmininginfo', this.getMiningInfo);
|
|
this.add('prioritisetransaction', this.prioritiseTransaction);
|
|
this.add('getwork', this.getWork);
|
|
this.add('getworklp', this.getWorkLongpoll);
|
|
this.add('submitwork', this.submitWork);
|
|
this.add('getblocktemplate', this.getBlockTemplate);
|
|
this.add('submitblock', this.submitBlock);
|
|
this.add('verifyblock', this.verifyBlock);
|
|
|
|
this.add('setgenerate', this.setGenerate);
|
|
this.add('getgenerate', this.getGenerate);
|
|
this.add('generate', this.generate);
|
|
this.add('generatetoaddress', this.generateToAddress);
|
|
|
|
this.add('estimatefee', this.estimateFee);
|
|
this.add('estimatepriority', this.estimatePriority);
|
|
this.add('estimatesmartfee', this.estimateSmartFee);
|
|
this.add('estimatesmartpriority', this.estimateSmartPriority);
|
|
|
|
this.add('getinfo', this.getInfo);
|
|
this.add('validateaddress', this.validateAddress);
|
|
this.add('createmultisig', this.createMultisig);
|
|
this.add('verifymessage', this.verifyMessage);
|
|
this.add('verifymessagewithname', this.verifyMessageWithName);
|
|
this.add('signmessagewithprivkey', this.signMessageWithPrivkey);
|
|
|
|
this.add('setmocktime', this.setMockTime);
|
|
|
|
this.add('getconnectioncount', this.getConnectionCount);
|
|
this.add('ping', this.ping);
|
|
this.add('getpeerinfo', this.getPeerInfo);
|
|
this.add('addnode', this.addNode);
|
|
this.add('disconnectnode', this.disconnectNode);
|
|
this.add('getaddednodeinfo', this.getAddedNodeInfo);
|
|
this.add('getnettotals', this.getNetTotals);
|
|
this.add('getnetworkinfo', this.getNetworkInfo);
|
|
this.add('setban', this.setBan);
|
|
this.add('listbanned', this.listBanned);
|
|
this.add('clearbanned', this.clearBanned);
|
|
|
|
this.add('getrawtransaction', this.getRawTransaction);
|
|
this.add('createrawtransaction', this.createRawTransaction);
|
|
this.add('decoderawtransaction', this.decodeRawTransaction);
|
|
this.add('decodescript', this.decodeScript);
|
|
this.add('decoderesource', this.decodeResource);
|
|
this.add('sendrawtransaction', this.sendRawTransaction);
|
|
this.add('signrawtransaction', this.signRawTransaction);
|
|
|
|
this.add('gettxoutproof', this.getTXOutProof);
|
|
this.add('verifytxoutproof', this.verifyTXOutProof);
|
|
|
|
this.add('getmemoryinfo', this.getMemoryInfo);
|
|
this.add('setloglevel', this.setLogLevel);
|
|
this.add('getnames', this.getNames);
|
|
this.add('getnameinfo', this.getNameInfo);
|
|
this.add('getnameresource', this.getNameResource);
|
|
this.add('getnameproof', this.getNameProof);
|
|
this.add('getdnssecproof', this.getDNSSECProof);
|
|
this.add('getnamebyhash', this.getNameByHash);
|
|
this.add('grindname', this.grindName);
|
|
this.add('sendrawclaim', this.sendRawClaim);
|
|
this.add('sendrawairdrop', this.sendRawAirdrop);
|
|
this.add('validateresource', this.validateResource);
|
|
|
|
this.add('resetrootcache', this.resetRootCache);
|
|
|
|
// Compat
|
|
// this.add('getnameinfo', this.getNameInfo);
|
|
// this.add('getnameresource', this.getNameResource);
|
|
// this.add('getnameproof', this.getNameProof);
|
|
}
|
|
|
|
/*
|
|
* Overall control/query calls
|
|
*/
|
|
|
|
async getInfo(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getinfo');
|
|
|
|
return {
|
|
version: pkg.version,
|
|
protocolversion: this.pool.options.version,
|
|
walletversion: 0,
|
|
balance: 0,
|
|
blocks: this.chain.height,
|
|
timeoffset: this.network.time.offset,
|
|
connections: this.pool.peers.size(),
|
|
proxy: '',
|
|
difficulty: toDifficulty(this.chain.tip.bits),
|
|
testnet: this.network !== Network.main,
|
|
keypoololdest: 0,
|
|
keypoolsize: 0,
|
|
unlocked_until: 0,
|
|
paytxfee: Amount.coin(this.network.feeRate, true),
|
|
relayfee: Amount.coin(this.network.minRelay, true),
|
|
errors: ''
|
|
};
|
|
}
|
|
|
|
async help(args, _help) {
|
|
if (args.length === 0)
|
|
return 'Select a command.';
|
|
|
|
const json = {
|
|
method: args[0],
|
|
params: []
|
|
};
|
|
|
|
return this.execute(json, true);
|
|
}
|
|
|
|
async stop(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'stop');
|
|
|
|
this.node.close().catch((err) => {
|
|
setImmediate(() => {
|
|
throw err;
|
|
});
|
|
});
|
|
|
|
return 'Stopping.';
|
|
}
|
|
|
|
/*
|
|
* P2P networking
|
|
*/
|
|
|
|
async getNetworkInfo(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnetworkinfo');
|
|
|
|
const hosts = this.pool.hosts;
|
|
const locals = [];
|
|
|
|
for (const local of hosts.local.values()) {
|
|
locals.push({
|
|
address: local.addr.host,
|
|
port: local.addr.port,
|
|
score: local.score
|
|
});
|
|
}
|
|
|
|
return {
|
|
version: pkg.version,
|
|
subversion: this.pool.options.agent,
|
|
protocolversion: this.pool.options.version,
|
|
identitykey: this.pool.hosts.brontide.getKey('base32'),
|
|
localservices: util.hex32(this.pool.options.services),
|
|
localservicenames: this.pool.getServiceNames(),
|
|
localrelay: !this.pool.options.noRelay,
|
|
timeoffset: this.network.time.offset,
|
|
networkactive: this.pool.connected,
|
|
connections: this.pool.peers.size(),
|
|
networks: [],
|
|
relayfee: Amount.coin(this.network.minRelay, true),
|
|
incrementalfee: 0,
|
|
localaddresses: locals,
|
|
warnings: ''
|
|
};
|
|
}
|
|
|
|
async addNode(args, help) {
|
|
if (help || args.length !== 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'addnode "node" "add|remove|onetry"');
|
|
|
|
const valid = new Validator(args);
|
|
const node = valid.str(0, '');
|
|
const cmd = valid.str(1, '');
|
|
|
|
switch (cmd) {
|
|
case 'add': {
|
|
this.pool.hosts.addNode(node);
|
|
; // fall through
|
|
}
|
|
case 'onetry': {
|
|
const addr = parseNetAddress(node, this.network);
|
|
|
|
if (!this.pool.peers.get(addr.hostname)) {
|
|
const peer = this.pool.createOutbound(addr);
|
|
this.pool.peers.add(peer);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 'remove': {
|
|
this.pool.hosts.removeNode(node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async disconnectNode(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'disconnectnode "node"');
|
|
|
|
const valid = new Validator(args);
|
|
const str = valid.str(0, '');
|
|
|
|
const addr = parseIP(str, this.network);
|
|
const peer = this.pool.peers.get(addr.hostname);
|
|
|
|
if (peer)
|
|
peer.destroy();
|
|
|
|
return null;
|
|
}
|
|
|
|
async getAddedNodeInfo(args, help) {
|
|
if (help || args.length > 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'getaddednodeinfo ( "node" )');
|
|
|
|
const hosts = this.pool.hosts;
|
|
const valid = new Validator(args);
|
|
const addr = valid.str(0, '');
|
|
|
|
let target;
|
|
if (args.length === 1)
|
|
target = parseIP(addr, this.network);
|
|
|
|
const result = [];
|
|
|
|
for (const node of hosts.nodes) {
|
|
if (target) {
|
|
if (node.host !== target.host)
|
|
continue;
|
|
|
|
if (node.port !== target.port)
|
|
continue;
|
|
}
|
|
|
|
const peer = this.pool.peers.get(node.hostname);
|
|
|
|
if (!peer || !peer.connected) {
|
|
result.push({
|
|
addednode: node.hostname,
|
|
connected: false,
|
|
addresses: []
|
|
});
|
|
continue;
|
|
}
|
|
|
|
result.push({
|
|
addednode: node.hostname,
|
|
connected: peer.connected,
|
|
addresses: [
|
|
{
|
|
address: peer.hostname(),
|
|
connected: peer.outbound
|
|
? 'outbound'
|
|
: 'inbound'
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
if (target && result.length === 0) {
|
|
throw new RPCError(errs.CLIENT_NODE_NOT_ADDED,
|
|
'Node has not been added.');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async getConnectionCount(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getconnectioncount');
|
|
|
|
return this.pool.peers.size();
|
|
}
|
|
|
|
async getNetTotals(args, help) {
|
|
let sent = 0;
|
|
let recv = 0;
|
|
|
|
if (help || args.length > 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnettotals');
|
|
|
|
for (let peer = this.pool.peers.head(); peer; peer = peer.next) {
|
|
sent += peer.socket.bytesWritten;
|
|
recv += peer.socket.bytesRead;
|
|
}
|
|
|
|
return {
|
|
totalbytesrecv: recv,
|
|
totalbytessent: sent,
|
|
timemillis: Date.now()
|
|
};
|
|
}
|
|
|
|
async getPeerInfo(args, help) {
|
|
if (help || args.length > 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'getpeerinfo ( "type" )');
|
|
|
|
const valid = new Validator(args);
|
|
const type = valid.str(0);
|
|
const peers = [];
|
|
|
|
for (let peer = this.pool.peers.head(); peer; peer = peer.next) {
|
|
if (!peer.connected)
|
|
continue;
|
|
|
|
if (type && peer.outbound !== (type === 'outbound'))
|
|
continue;
|
|
|
|
const offset = this.network.time.known.get(peer.hostname()) || 0;
|
|
const hashes = [];
|
|
|
|
for (const hash in peer.blockMap.keys())
|
|
hashes.push(hash.toString('hex'));
|
|
|
|
peer.getName();
|
|
|
|
peers.push({
|
|
id: peer.id,
|
|
addr: peer.hostname(),
|
|
addrlocal: !peer.local.isNull()
|
|
? peer.local.hostname
|
|
: undefined,
|
|
name: peer.name || undefined,
|
|
services: util.hex32(peer.services),
|
|
servicenames: peer.getServiceNames(),
|
|
relaytxes: !peer.noRelay,
|
|
lastsend: peer.lastSend / 1000 | 0,
|
|
lastrecv: peer.lastRecv / 1000 | 0,
|
|
bytessent: peer.socket.bytesWritten,
|
|
bytesrecv: peer.socket.bytesRead,
|
|
conntime: peer.time !== 0 ? (Date.now() - peer.time) / 1000 | 0 : 0,
|
|
timeoffset: offset,
|
|
pingtime: peer.lastPong !== -1
|
|
? (peer.lastPong - peer.lastPing) / 1000
|
|
: -1,
|
|
minping: peer.minPing !== -1 ? peer.minPing / 1000 : -1,
|
|
version: peer.version,
|
|
subver: peer.agent,
|
|
inbound: !peer.outbound,
|
|
startingheight: peer.height,
|
|
besthash: peer.bestHash.toString('hex'),
|
|
bestheight: peer.bestHeight,
|
|
banscore: peer.banScore,
|
|
inflight: hashes,
|
|
whitelisted: false
|
|
});
|
|
}
|
|
|
|
return peers;
|
|
}
|
|
|
|
async ping(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'ping');
|
|
|
|
for (let peer = this.pool.peers.head(); peer; peer = peer.next)
|
|
peer.sendPing();
|
|
|
|
return null;
|
|
}
|
|
|
|
async setBan(args, help) {
|
|
const valid = new Validator(args);
|
|
const str = valid.str(0, '');
|
|
const action = valid.str(1, '');
|
|
|
|
if (help
|
|
|| args.length < 2
|
|
|| (action !== 'add' && action !== 'remove')) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'setban "ip(/netmask)" "add|remove" (bantime) (absolute)');
|
|
}
|
|
|
|
const addr = parseNetAddress(str, this.network);
|
|
|
|
switch (action) {
|
|
case 'add':
|
|
this.pool.ban(addr);
|
|
break;
|
|
case 'remove':
|
|
this.pool.unban(addr);
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async listBanned(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'listbanned');
|
|
|
|
const banned = [];
|
|
|
|
for (const [host, time] of this.pool.hosts.banned) {
|
|
banned.push({
|
|
address: host,
|
|
banned_until: time + this.pool.options.banTime,
|
|
ban_created: time,
|
|
ban_reason: ''
|
|
});
|
|
}
|
|
|
|
return banned;
|
|
}
|
|
|
|
async clearBanned(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'clearbanned');
|
|
|
|
this.pool.hosts.clearBanned();
|
|
|
|
return null;
|
|
}
|
|
|
|
/* Block chain and UTXO */
|
|
async getBlockchainInfo(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getblockchaininfo');
|
|
|
|
return {
|
|
chain: this.network.type !== 'testnet'
|
|
? this.network.type
|
|
: 'test',
|
|
blocks: this.chain.height,
|
|
headers: this.chain.height,
|
|
bestblockhash: this.chain.tip.hash.toString('hex'),
|
|
treeroot: this.chain.tip.treeRoot.toString('hex'),
|
|
difficulty: toDifficulty(this.chain.tip.bits),
|
|
mediantime: await this.chain.getMedianTime(this.chain.tip),
|
|
verificationprogress: this.chain.getProgress(),
|
|
chainwork: this.chain.tip.chainwork.toString('hex', 64),
|
|
pruned: this.chain.options.prune,
|
|
softforks: await this.getSoftforks(),
|
|
deflationary: this.chain.height >= this.network.deflationHeight,
|
|
pruneheight: this.chain.options.prune
|
|
? Math.max(0, this.chain.height - this.network.block.keepBlocks)
|
|
: null
|
|
};
|
|
}
|
|
|
|
async getBestBlockHash(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getbestblockhash');
|
|
|
|
return this.chain.tip.hash.toString('hex');
|
|
}
|
|
|
|
async getBlockCount(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getblockcount');
|
|
|
|
return this.chain.tip.height;
|
|
}
|
|
|
|
async getBlock(args, help) {
|
|
if (help || args.length < 1 || args.length > 3)
|
|
throw new RPCError(errs.MISC_ERROR, 'getblock "hash" ( verbose )');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const verbose = valid.bool(1, true);
|
|
const details = valid.bool(2, false);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.');
|
|
|
|
const entry = await this.chain.getEntry(hash);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not found');
|
|
|
|
const block = await this.chain.getBlock(entry.hash);
|
|
|
|
if (!block) {
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not available (spv mode)');
|
|
|
|
if (this.chain.options.prune) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'Block not available (pruned data)');
|
|
}
|
|
|
|
throw new RPCError(errs.MISC_ERROR, 'Can\'t read block from disk');
|
|
}
|
|
|
|
if (!verbose)
|
|
return block.toHex();
|
|
|
|
return this.blockToJSON(entry, block, details);
|
|
}
|
|
|
|
async getBlockByHeight(args, help) {
|
|
if (help || args.length < 1 || args.length > 3) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'getblockbyheight "height" ( verbose )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const height = valid.u32(0, -1);
|
|
const verbose = valid.bool(1, true);
|
|
const details = valid.bool(2, false);
|
|
|
|
if (height === -1)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid block height.');
|
|
|
|
const entry = await this.chain.getEntry(height);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not found');
|
|
|
|
const block = await this.chain.getBlock(entry.hash);
|
|
|
|
if (!block) {
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not available (spv mode)');
|
|
|
|
if (this.chain.options.prune) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'Block not available (pruned data)');
|
|
}
|
|
|
|
throw new RPCError(errs.DATABASE_ERROR, 'Can\'t read block from disk');
|
|
}
|
|
|
|
if (!verbose)
|
|
return block.toHex();
|
|
|
|
return this.blockToJSON(entry, block, details);
|
|
}
|
|
|
|
async getBlockHash(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'getblockhash index');
|
|
|
|
const valid = new Validator(args);
|
|
const height = valid.u32(0);
|
|
|
|
if (height == null || height > this.chain.height)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Block height out of range.');
|
|
|
|
const hash = await this.chain.getHash(height);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.MISC_ERROR, 'Not found.');
|
|
|
|
return hash.toString('hex');
|
|
}
|
|
|
|
async getBlockHeader(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getblockheader "hash" ( verbose )');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const verbose = valid.bool(1, true);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.MISC_ERROR, 'Invalid block hash.');
|
|
|
|
const entry = await this.chain.getEntry(hash);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not found');
|
|
|
|
if (!verbose)
|
|
return entry.encode().toString('hex', 36, 36 + consensus.HEADER_SIZE);
|
|
|
|
return this.headerToJSON(entry);
|
|
}
|
|
|
|
async getChainTips(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getchaintips');
|
|
|
|
const tips = await this.chain.getTips();
|
|
const result = [];
|
|
|
|
for (const hash of tips) {
|
|
const entry = await this.chain.getEntry(hash);
|
|
|
|
assert(entry);
|
|
|
|
const fork = await this.findFork(entry);
|
|
const main = await this.chain.isMainChain(entry);
|
|
|
|
result.push({
|
|
height: entry.height,
|
|
hash: entry.hash.toString('hex'),
|
|
branchlen: entry.height - fork.height,
|
|
status: main ? 'active' : 'valid-headers'
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async getDifficulty(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getdifficulty');
|
|
|
|
return toDifficulty(this.chain.tip.bits);
|
|
}
|
|
|
|
async getMempoolInfo(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getmempoolinfo');
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
return {
|
|
size: this.mempool.map.size,
|
|
bytes: this.mempool.getSize(),
|
|
usage: this.mempool.getSize(),
|
|
maxmempool: this.mempool.options.maxSize,
|
|
mempoolminfee: Amount.coin(this.mempool.options.minRelay, true)
|
|
};
|
|
}
|
|
|
|
async getMempoolAncestors(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getmempoolancestors txid (verbose)');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const verbose = valid.bool(1, false);
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.');
|
|
|
|
const entry = this.mempool.getEntry(hash);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.');
|
|
|
|
const entries = this.mempool.getAncestors(entry);
|
|
const out = [];
|
|
|
|
if (verbose) {
|
|
for (const entry of entries)
|
|
out.push(this.entryToJSON(entry));
|
|
} else {
|
|
for (const entry of entries)
|
|
out.push(entry.txid());
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
async getMempoolDescendants(args, help) {
|
|
if (help || args.length < 1 || args.length > 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'getmempooldescendants txid (verbose)');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const verbose = valid.bool(1, false);
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.');
|
|
|
|
const entry = this.mempool.getEntry(hash);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.');
|
|
|
|
const entries = this.mempool.getDescendants(entry);
|
|
const out = [];
|
|
|
|
if (verbose) {
|
|
for (const entry of entries)
|
|
out.push(this.entryToJSON(entry));
|
|
} else {
|
|
for (const entry of entries)
|
|
out.push(entry.txid());
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
async getMempoolEntry(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'getmempoolentry txid');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.');
|
|
|
|
const entry = this.mempool.getEntry(hash);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.');
|
|
|
|
return this.entryToJSON(entry);
|
|
}
|
|
|
|
async getRawMempool(args, help) {
|
|
if (help || args.length > 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'getrawmempool ( verbose )');
|
|
|
|
const valid = new Validator(args);
|
|
const verbose = valid.bool(0, false);
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
if (verbose) {
|
|
const out = Object.create(null);
|
|
|
|
for (const entry of this.mempool.map.values())
|
|
out[entry.txid()] = this.entryToJSON(entry);
|
|
|
|
return out;
|
|
}
|
|
|
|
const hashes = this.mempool.getSnapshot();
|
|
|
|
return hashes.map(hash => hash.toString('hex'));
|
|
}
|
|
|
|
async getTXOut(args, help) {
|
|
if (help || args.length < 2 || args.length > 3) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'gettxout "txid" n ( includemempool )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const index = valid.u32(1);
|
|
const mempool = valid.bool(2, true);
|
|
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot get coins in SPV mode.');
|
|
|
|
if (this.chain.options.prune)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot get coins when pruned.');
|
|
|
|
if (!hash || index == null)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.');
|
|
|
|
let coin;
|
|
if (mempool) {
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
coin = this.mempool.getCoin(hash, index);
|
|
}
|
|
|
|
if (!coin)
|
|
coin = await this.chain.getCoin(hash, index);
|
|
|
|
if (!coin)
|
|
return null;
|
|
|
|
return {
|
|
bestblock: this.chain.tip.hash.toString('hex'),
|
|
confirmations: coin.getDepth(this.chain.height),
|
|
value: Amount.coin(coin.value, true),
|
|
address: this.addrToJSON(coin.address),
|
|
version: coin.version,
|
|
coinbase: coin.coinbase
|
|
};
|
|
}
|
|
|
|
async getTXOutProof(args, help) {
|
|
if (help || (args.length !== 1 && args.length !== 2)) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'gettxoutproof ["txid",...] ( blockhash )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const txids = valid.array(0);
|
|
const hash = valid.bhash(1);
|
|
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot get coins in SPV mode.');
|
|
|
|
if (this.chain.options.prune)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot get coins when pruned.');
|
|
|
|
if (!txids || txids.length === 0)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid TXIDs.');
|
|
|
|
const items = new Validator(txids);
|
|
const set = new BufferSet();
|
|
const hashes = [];
|
|
|
|
let last = null;
|
|
|
|
for (let i = 0; i < txids.length; i++) {
|
|
const hash = items.bhash(i);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.');
|
|
|
|
if (set.has(hash))
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate txid.');
|
|
|
|
set.add(hash);
|
|
hashes.push(hash);
|
|
|
|
last = hash;
|
|
}
|
|
|
|
let block = null;
|
|
|
|
if (hash) {
|
|
block = await this.chain.getBlock(hash);
|
|
} else if (this.chain.options.indexTX) {
|
|
const tx = await this.chain.getMeta(last);
|
|
if (tx)
|
|
block = await this.chain.getBlock(tx.block);
|
|
} else {
|
|
const coin = await this.chain.getCoin(last, 0);
|
|
if (coin)
|
|
block = await this.chain.getBlock(coin.height);
|
|
}
|
|
|
|
if (!block)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not found.');
|
|
|
|
const whashes = [];
|
|
|
|
for (const hash of hashes) {
|
|
const index = block.indexOf(hash);
|
|
|
|
if (index === -1) {
|
|
throw new RPCError(errs.VERIFY_ERROR,
|
|
'Block does not contain all txids.');
|
|
}
|
|
|
|
const tx = block.txs[index];
|
|
|
|
whashes.push(tx.hash());
|
|
}
|
|
|
|
const mblock = MerkleBlock.fromHashes(block, whashes);
|
|
|
|
return mblock.toHex();
|
|
}
|
|
|
|
async verifyTXOutProof(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'verifytxoutproof "proof"');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.');
|
|
|
|
const block = MerkleBlock.decode(data);
|
|
|
|
if (!block.verify())
|
|
return [];
|
|
|
|
const entry = await this.chain.getEntry(block.hash());
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Block not found in chain.');
|
|
|
|
const tree = block.getTree();
|
|
const out = [];
|
|
|
|
for (const hash of tree.matches)
|
|
out.push(hash.toString('hex'));
|
|
|
|
return out;
|
|
}
|
|
|
|
async getTXOutSetInfo(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'gettxoutsetinfo');
|
|
|
|
if (this.chain.options.spv) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'Chainstate not available (SPV mode).');
|
|
}
|
|
|
|
return {
|
|
height: this.chain.height,
|
|
bestblock: this.chain.tip.hash.toString('hex'),
|
|
transactions: this.chain.db.state.tx,
|
|
txouts: this.chain.db.state.coin,
|
|
bytes_serialized: 0,
|
|
hash_serialized: 0,
|
|
total_amount: Amount.coin(this.chain.db.state.value, true),
|
|
total_burned: Amount.coin(this.chain.db.state.burned, true)
|
|
};
|
|
}
|
|
|
|
async pruneBlockchain(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'pruneblockchain');
|
|
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot prune chain in SPV mode.');
|
|
|
|
if (this.chain.options.prune)
|
|
throw new RPCError(errs.MISC_ERROR, 'Chain is already pruned.');
|
|
|
|
if (this.chain.height < this.network.block.pruneAfterHeight)
|
|
throw new RPCError(errs.MISC_ERROR, 'Chain is too short for pruning.');
|
|
|
|
try {
|
|
await this.chain.prune();
|
|
} catch (e) {
|
|
throw new RPCError(errs.DATABASE_ERROR, e.message);
|
|
}
|
|
}
|
|
|
|
async compactTree(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'compacttree');
|
|
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot compact tree in SPV mode.');
|
|
|
|
try {
|
|
await this.chain.compactTree();
|
|
} catch (e) {
|
|
throw new RPCError(errs.DATABASE_ERROR, e.message);
|
|
}
|
|
}
|
|
|
|
async reconstructTree(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'reconstructtree');
|
|
|
|
if (this.chain.options.spv) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'Cannot reconstruct tree in SPV mode.');
|
|
}
|
|
|
|
if (this.chain.options.prune) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'Cannot reconstruct tree in pruned node.');
|
|
}
|
|
|
|
try {
|
|
await this.chain.reconstructTree();
|
|
} catch (e) {
|
|
throw new RPCError(errs.DATABASE_ERROR, e.message);
|
|
}
|
|
}
|
|
|
|
async verifyChain(args, help) {
|
|
if (help || args.length > 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'verifychain ( checklevel numblocks )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const level = valid.u32(0);
|
|
const blocks = valid.u32(1);
|
|
|
|
if (level == null || blocks == null)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Missing parameters.');
|
|
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot verify chain in SPV mode.');
|
|
|
|
if (this.chain.options.prune)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot verify chain when pruned.');
|
|
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Mining
|
|
*/
|
|
|
|
async handleWork(data, mask) {
|
|
const unlock = await this.locker.lock();
|
|
try {
|
|
return await this._handleWork(data, mask);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
}
|
|
|
|
async _handleWork(data, mask) {
|
|
if (data.length !== 256)
|
|
return [false, 'invalid-data-length'];
|
|
|
|
const hdr = Headers.fromMiner(data);
|
|
const maskHash = blake2b.multi(hdr.prevBlock, mask);
|
|
|
|
if (!hdr.maskHash().equals(maskHash))
|
|
return [false, 'bad-maskhash'];
|
|
|
|
const attempt = this.merkleMap.get(hdr.witnessRoot);
|
|
|
|
if (!attempt)
|
|
return [false, 'stale'];
|
|
|
|
if (!hdr.prevBlock.equals(attempt.prevBlock)
|
|
|| hdr.bits !== attempt.bits) {
|
|
return [false, 'stale'];
|
|
}
|
|
|
|
const {nonce, time, extraNonce} = hdr;
|
|
const proof = attempt.getProof(nonce, time, extraNonce, mask);
|
|
|
|
if (!proof.verify(attempt.target, this.network))
|
|
return [false, 'bad-diffbits'];
|
|
|
|
const block = attempt.commit(proof);
|
|
|
|
let entry;
|
|
try {
|
|
entry = await this.chain.add(block);
|
|
} catch (err) {
|
|
if (err.type === 'VerifyError') {
|
|
this.logger.warning('RPC block rejected: %x (%s).',
|
|
block.hash(), err.reason);
|
|
return [false, err.reason];
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
if (!entry) {
|
|
this.logger.warning('RPC block rejected: %x (bad-prevblk).',
|
|
block.hash());
|
|
return [false, 'bad-prevblk'];
|
|
}
|
|
|
|
return [true, 'valid'];
|
|
}
|
|
|
|
async createWork() {
|
|
const unlock = await this.locker.lock();
|
|
try {
|
|
return await this._createWork();
|
|
} finally {
|
|
unlock();
|
|
}
|
|
}
|
|
|
|
async _createWork() {
|
|
const attempt = await this.updateWork();
|
|
const time = attempt.time;
|
|
const nonce = consensus.ZERO_NONCE;
|
|
const mask = consensus.ZERO_HASH;
|
|
const data = attempt.getHeader(0, time, nonce, mask);
|
|
|
|
return {
|
|
network: this.network.type,
|
|
data: data.toString('hex'),
|
|
target: attempt.target.toString('hex'),
|
|
height: attempt.height,
|
|
time: this.network.now(),
|
|
fee: attempt.fees
|
|
};
|
|
}
|
|
|
|
async getWorkLongpoll(args, help) {
|
|
await this.longpoll();
|
|
return this.createWork();
|
|
}
|
|
|
|
async getWork(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getwork');
|
|
|
|
return this.createWork();
|
|
}
|
|
|
|
async submitWork(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'submitwork ( "data" "mask" )');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid work data.');
|
|
|
|
let mask = consensus.ZERO_HASH;
|
|
|
|
if (args.length === 2) {
|
|
mask = valid.bhash(1);
|
|
|
|
if (!mask)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid mask.');
|
|
}
|
|
|
|
return this.handleWork(data, mask);
|
|
}
|
|
|
|
async submitBlock(args, help) {
|
|
if (help || args.length < 1 || args.length > 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'submitblock "hexdata" ( "jsonparametersobject" )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
const block = Block.decode(data);
|
|
|
|
return this.addBlock(block);
|
|
}
|
|
|
|
async getBlockTemplate(args, help) {
|
|
if (help || args.length > 1) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'getblocktemplate ( "jsonrequestobject" )');
|
|
}
|
|
|
|
const validator = new Validator(args);
|
|
const options = validator.obj(0, {});
|
|
const valid = new Validator(options);
|
|
const mode = valid.str('mode', 'template');
|
|
|
|
if (mode !== 'template' && mode !== 'proposal')
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid mode.');
|
|
|
|
if (mode === 'proposal') {
|
|
const data = valid.buf('data');
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Missing data parameter.');
|
|
|
|
const block = Block.decode(data);
|
|
|
|
if (!block.prevBlock.equals(this.chain.tip.hash))
|
|
return 'inconclusive-not-best-prevblk';
|
|
|
|
try {
|
|
await this.chain.verifyBlock(block);
|
|
} catch (e) {
|
|
if (e.type === 'VerifyError')
|
|
return e.reason;
|
|
throw e;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
let maxVersion = valid.u32('maxversion', -1);
|
|
let rules = valid.array('rules');
|
|
|
|
if (rules)
|
|
maxVersion = -1;
|
|
|
|
const capabilities = valid.array('capabilities');
|
|
let coinbase = false;
|
|
|
|
if (capabilities) {
|
|
let txnCap = false;
|
|
let valueCap = false;
|
|
|
|
for (const capability of capabilities) {
|
|
if (typeof capability !== 'string')
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid capability.');
|
|
|
|
switch (capability) {
|
|
case 'coinbasetxn':
|
|
txnCap = true;
|
|
break;
|
|
case 'coinbasevalue':
|
|
// Prefer value if they support it.
|
|
valueCap = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (txnCap && !valueCap) {
|
|
if (this.miner.addresses.length === 0) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'No addresses available for coinbase.');
|
|
}
|
|
coinbase = true;
|
|
}
|
|
}
|
|
|
|
if (!this.network.selfConnect) {
|
|
if (this.pool.peers.size() === 0) {
|
|
throw new RPCError(errs.CLIENT_NOT_CONNECTED,
|
|
'Node is not connected!');
|
|
}
|
|
|
|
if (!this.chain.synced) {
|
|
throw new RPCError(errs.CLIENT_IN_INITIAL_DOWNLOAD,
|
|
'Node is downloading blocks...');
|
|
}
|
|
}
|
|
|
|
const lpid = valid.str('longpollid');
|
|
|
|
if (lpid)
|
|
await this.handleLongpoll(lpid);
|
|
|
|
if (!rules)
|
|
rules = [];
|
|
|
|
return this.createTemplate(maxVersion, coinbase, rules);
|
|
}
|
|
|
|
async createTemplate(maxVersion, coinbase, rules) {
|
|
const unlock = await this.locker.lock();
|
|
try {
|
|
return await this._createTemplate(maxVersion, coinbase, rules);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
}
|
|
|
|
async _createTemplate(maxVersion, coinbase, rules) {
|
|
const attempt = await this.getTemplate();
|
|
const scale = attempt.witness ? 1 : consensus.WITNESS_SCALE_FACTOR;
|
|
|
|
// Default mutable fields.
|
|
const mutable = ['time', 'transactions', 'prevblock'];
|
|
|
|
// The miner doesn't support
|
|
// versionbits. Force them to
|
|
// encode our version.
|
|
if (maxVersion >= 2)
|
|
mutable.push('version/force');
|
|
|
|
// Allow the miner to change
|
|
// our provided coinbase.
|
|
// Note that these are implied
|
|
// without `coinbasetxn`.
|
|
if (coinbase) {
|
|
mutable.push('coinbase');
|
|
mutable.push('coinbase/append');
|
|
mutable.push('generation');
|
|
}
|
|
|
|
// Build an index of every transaction.
|
|
const index = new BufferMap();
|
|
for (let i = 0; i < attempt.items.length; i++) {
|
|
const entry = attempt.items[i];
|
|
index.set(entry.hash, i + 1);
|
|
}
|
|
|
|
// Calculate dependencies for each transaction.
|
|
const txs = [];
|
|
for (let i = 0; i < attempt.items.length; i++) {
|
|
const entry = attempt.items[i];
|
|
const tx = entry.tx;
|
|
const deps = [];
|
|
|
|
for (let j = 0; j < tx.inputs.length; j++) {
|
|
const input = tx.inputs[j];
|
|
const dep = index.get(input.prevout.hash);
|
|
|
|
if (dep == null)
|
|
continue;
|
|
|
|
if (deps.indexOf(dep) === -1) {
|
|
assert(dep < i + 1);
|
|
deps.push(dep);
|
|
}
|
|
}
|
|
|
|
txs.push({
|
|
data: tx.toHex(),
|
|
txid: tx.txid(),
|
|
hash: tx.wtxid(),
|
|
depends: deps,
|
|
fee: entry.fee,
|
|
sigops: entry.sigops / scale | 0,
|
|
weight: tx.getWeight()
|
|
});
|
|
}
|
|
|
|
// Calculate version based on given rules.
|
|
let version = attempt.version;
|
|
|
|
const vbavailable = {};
|
|
const vbrules = [];
|
|
|
|
for (const deploy of this.network.deploys) {
|
|
const state = await this.chain.getState(this.chain.tip, deploy);
|
|
|
|
let name = deploy.name;
|
|
|
|
switch (state) {
|
|
case common.thresholdStates.DEFINED:
|
|
case common.thresholdStates.FAILED:
|
|
break;
|
|
case common.thresholdStates.LOCKED_IN:
|
|
version |= 1 << deploy.bit;
|
|
case common.thresholdStates.STARTED:
|
|
if (!deploy.force) {
|
|
if (rules.indexOf(name) === -1)
|
|
version &= ~(1 << deploy.bit);
|
|
if (deploy.required)
|
|
name = '!' + name;
|
|
}
|
|
vbavailable[name] = deploy.bit;
|
|
break;
|
|
case common.thresholdStates.ACTIVE:
|
|
if (!deploy.force && deploy.required) {
|
|
if (rules.indexOf(name) === -1) {
|
|
throw new RPCError(errs.INVALID_PARAMETER,
|
|
`Client must support ${name}.`);
|
|
}
|
|
name = '!' + name;
|
|
}
|
|
vbrules.push(name);
|
|
break;
|
|
default:
|
|
assert(false, 'Bad state.');
|
|
break;
|
|
}
|
|
}
|
|
|
|
version >>>= 0;
|
|
|
|
const json = {
|
|
capabilities: ['proposal'],
|
|
mutable: mutable,
|
|
version: version,
|
|
rules: vbrules,
|
|
vbavailable: vbavailable,
|
|
vbrequired: 0,
|
|
height: attempt.height,
|
|
previousblockhash: attempt.prevBlock.toString('hex'),
|
|
merkleroot: undefined,
|
|
witnessroot: undefined,
|
|
treeroot: attempt.treeRoot.toString('hex'),
|
|
reservedroot: attempt.reservedRoot.toString('hex'),
|
|
mask: consensus.ZERO_HASH.toString('hex'), // Compat.
|
|
target: attempt.target.toString('hex'),
|
|
bits: util.hex32(attempt.bits),
|
|
noncerange:
|
|
Array(consensus.NONCE_SIZE + 1).join('00')
|
|
+ Array(consensus.NONCE_SIZE + 1).join('ff'),
|
|
curtime: attempt.time,
|
|
mintime: attempt.mtp + 1,
|
|
maxtime: attempt.time + 7200,
|
|
expires: attempt.time + 7200,
|
|
sigoplimit: consensus.MAX_BLOCK_SIGOPS,
|
|
// sizelimit: consensus.MAX_RAW_BLOCK_SIZE,
|
|
sizelimit: consensus.MAX_BLOCK_SIZE,
|
|
weightlimit: consensus.MAX_BLOCK_WEIGHT,
|
|
longpollid: this.chain.tip.hash.toString('hex')
|
|
+ util.hex32(this.totalTX()),
|
|
submitold: false,
|
|
coinbaseaux: {
|
|
flags: attempt.coinbaseFlags.toString('hex')
|
|
},
|
|
coinbasevalue: attempt.getReward(),
|
|
coinbasetxn: undefined,
|
|
claims: attempt.claims.map((claim) => {
|
|
let value = claim.value;
|
|
let fee = claim.fee;
|
|
|
|
// Account for mining software which creates its own
|
|
// coinbase with something other than `coinbasevalue`.
|
|
if (attempt.height >= this.network.deflationHeight) {
|
|
if (claim.commitHeight !== 1) {
|
|
value = claim.value - claim.fee;
|
|
fee = 0;
|
|
}
|
|
}
|
|
|
|
return {
|
|
data: claim.blob.toString('hex'),
|
|
name: claim.name.toString('binary'),
|
|
namehash: claim.nameHash.toString('hex'),
|
|
version: claim.address.version,
|
|
hash: claim.address.hash.toString('hex'),
|
|
value: value,
|
|
fee: fee,
|
|
weak: claim.weak,
|
|
commitHash: claim.commitHash.toString('hex'),
|
|
commitHeight: claim.commitHeight,
|
|
weight: claim.getWeight()
|
|
};
|
|
}),
|
|
airdrops: attempt.airdrops.map((airdrop) => {
|
|
return {
|
|
data: airdrop.blob.toString('hex'),
|
|
position: airdrop.position,
|
|
version: airdrop.address.version,
|
|
address: airdrop.address.hash.toString('hex'),
|
|
value: airdrop.value,
|
|
fee: airdrop.fee,
|
|
rate: airdrop.rate,
|
|
weak: airdrop.weak
|
|
};
|
|
}),
|
|
transactions: txs
|
|
};
|
|
|
|
// The client wants a coinbasetxn
|
|
// instead of a coinbasevalue.
|
|
if (coinbase) {
|
|
const tx = attempt.coinbase;
|
|
|
|
json.merkleroot = attempt.merkleRoot.toString('hex');
|
|
json.witnessroot = attempt.witnessRoot.toString('hex');
|
|
|
|
json.coinbasetxn = {
|
|
data: tx.toHex(),
|
|
txid: tx.txid(),
|
|
hash: tx.wtxid(),
|
|
depends: [],
|
|
fee: 0,
|
|
sigops: tx.getSigops() / scale | 0,
|
|
weight: tx.getWeight()
|
|
};
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
async getMiningInfo(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getmininginfo');
|
|
|
|
const attempt = this.attempt;
|
|
|
|
let size = 0;
|
|
let weight = 0;
|
|
let txs = 0;
|
|
let diff = 0;
|
|
|
|
if (attempt) {
|
|
weight = attempt.weight;
|
|
txs = attempt.items.length + 1;
|
|
diff = attempt.getDifficulty();
|
|
size = 1000;
|
|
for (const item of attempt.items)
|
|
size += item.tx.getBaseSize();
|
|
}
|
|
|
|
return {
|
|
blocks: this.chain.height,
|
|
currentblocksize: size,
|
|
currentblockweight: weight,
|
|
currentblocktx: txs,
|
|
difficulty: diff,
|
|
errors: '',
|
|
genproclimit: this.procLimit,
|
|
networkhashps: await this.getHashRate(120),
|
|
pooledtx: this.totalTX(),
|
|
testnet: this.network !== Network.main,
|
|
chain: this.network.type !== 'testnet'
|
|
? this.network.type
|
|
: 'test',
|
|
generate: this.mining
|
|
};
|
|
}
|
|
|
|
async getNetworkHashPS(args, help) {
|
|
if (help || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnetworkhashps ( blocks height )');
|
|
|
|
const valid = new Validator(args);
|
|
const lookup = valid.u32(0, 120);
|
|
const height = valid.u32(1);
|
|
|
|
return this.getHashRate(lookup, height);
|
|
}
|
|
|
|
async prioritiseTransaction(args, help) {
|
|
if (help || args.length !== 3) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'prioritisetransaction <txid> <priority delta> <fee delta>');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const pri = valid.i64(1);
|
|
const fee = valid.i64(2);
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID');
|
|
|
|
if (pri == null || fee == null)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid fee or priority.');
|
|
|
|
const entry = this.mempool.getEntry(hash);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.MISC_ERROR, 'Transaction not in mempool.');
|
|
|
|
this.mempool.prioritise(entry, pri, fee);
|
|
|
|
return true;
|
|
}
|
|
|
|
async verifyBlock(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'verifyblock "block-hex"');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid block hex.');
|
|
|
|
if (this.chain.options.spv)
|
|
throw new RPCError(errs.MISC_ERROR, 'Cannot verify block in SPV mode.');
|
|
|
|
const block = Block.decode(data);
|
|
|
|
try {
|
|
await this.chain.verifyBlock(block);
|
|
} catch (e) {
|
|
if (e.type === 'VerifyError')
|
|
return e.reason;
|
|
throw e;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Coin generation
|
|
*/
|
|
|
|
async getGenerate(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getgenerate');
|
|
return this.mining;
|
|
}
|
|
|
|
async setGenerate(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'setgenerate mine ( proclimit )');
|
|
|
|
const valid = new Validator(args);
|
|
const mine = valid.bool(0, false);
|
|
const limit = valid.u32(1, 0);
|
|
|
|
if (mine && this.miner.addresses.length === 0) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'No addresses available for coinbase.');
|
|
}
|
|
|
|
this.mining = mine;
|
|
this.procLimit = limit;
|
|
|
|
if (mine) {
|
|
this.miner.cpu.start();
|
|
return true;
|
|
}
|
|
|
|
await this.miner.cpu.stop();
|
|
|
|
return false;
|
|
}
|
|
|
|
async generate(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'generate numblocks ( maxtries )');
|
|
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0, 1);
|
|
const tries = valid.u32(1);
|
|
|
|
if (this.miner.addresses.length === 0) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'No addresses available for coinbase.');
|
|
}
|
|
|
|
return this.mineBlocks(blocks, null, tries);
|
|
}
|
|
|
|
async generateToAddress(args, help) {
|
|
if (help || args.length < 2 || args.length > 3) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'generatetoaddress numblocks address ( maxtries )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0, 1);
|
|
const str = valid.str(1, '');
|
|
const tries = valid.u32(2);
|
|
|
|
const addr = parseAddress(str, this.network);
|
|
|
|
return this.mineBlocks(blocks, addr, tries);
|
|
}
|
|
|
|
/*
|
|
* Raw transactions
|
|
*/
|
|
|
|
async createRawTransaction(args, help) {
|
|
if (help || args.length < 2 || args.length > 3) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'createrawtransaction'
|
|
+ ' [{"txid":"id","vout":n},...]'
|
|
+ ' {"address":amount,"data":"hex",...}'
|
|
+ ' ( locktime )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const inputs = valid.array(0);
|
|
const sendTo = valid.obj(1);
|
|
const locktime = valid.u32(2);
|
|
|
|
if (!inputs || !sendTo) {
|
|
throw new RPCError(errs.TYPE_ERROR,
|
|
'Invalid parameters (inputs and sendTo).');
|
|
}
|
|
|
|
const tx = new MTX();
|
|
|
|
if (locktime != null)
|
|
tx.locktime = locktime;
|
|
|
|
for (const obj of inputs) {
|
|
const valid = new Validator(obj);
|
|
const hash = valid.bhash('txid');
|
|
const index = valid.u32('vout');
|
|
|
|
let sequence = valid.u32('sequence', 0xffffffff);
|
|
|
|
if (tx.locktime)
|
|
sequence -= 1;
|
|
|
|
if (!hash || index == null)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid outpoint.');
|
|
|
|
const input = new Input();
|
|
input.prevout.hash = hash;
|
|
input.prevout.index = index;
|
|
input.sequence = sequence;
|
|
|
|
tx.inputs.push(input);
|
|
}
|
|
|
|
const sends = new Validator(sendTo);
|
|
const uniq = new Set();
|
|
|
|
for (const key of Object.keys(sendTo)) {
|
|
if (key === 'data') {
|
|
const value = sends.buf(key);
|
|
|
|
if (!value)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid nulldata..');
|
|
|
|
const output = new Output();
|
|
output.value = 0;
|
|
output.address.fromNulldata(value);
|
|
tx.outputs.push(output);
|
|
|
|
continue;
|
|
}
|
|
|
|
const addr = parseAddress(key, this.network);
|
|
const b58 = addr.toString(this.network);
|
|
|
|
if (uniq.has(b58))
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Duplicate address');
|
|
|
|
uniq.add(b58);
|
|
|
|
const value = sends.ufixed(key, EXP);
|
|
|
|
if (value == null)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid output value.');
|
|
|
|
const output = new Output();
|
|
output.value = value;
|
|
output.address = addr;
|
|
|
|
tx.outputs.push(output);
|
|
}
|
|
|
|
return tx.toHex();
|
|
}
|
|
|
|
async decodeRawTransaction(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'decoderawtransaction "hexstring"');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.');
|
|
|
|
const tx = TX.decode(data);
|
|
|
|
return this.txToJSON(tx);
|
|
}
|
|
|
|
async decodeScript(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'decodescript "hex"');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
const script = new Script();
|
|
|
|
if (data)
|
|
script.decode(data);
|
|
|
|
const addr = Address.fromScripthash(script.sha3());
|
|
|
|
const json = this.scriptToJSON(script);
|
|
json.p2sh = addr.toString(this.network);
|
|
|
|
return json;
|
|
}
|
|
|
|
async decodeResource(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'decoderesource "hex"');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
const res = Resource.decode(data);
|
|
return res.toJSON();
|
|
}
|
|
|
|
async getRawTransaction(args, help) {
|
|
if (help || args.length < 1 || args.length > 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'getrawtransaction "txid" ( verbose )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const verbose = valid.bool(1, false);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid TXID.');
|
|
|
|
const meta = await this.node.getMeta(hash);
|
|
|
|
if (!meta)
|
|
throw new RPCError(errs.MISC_ERROR, 'Transaction not found.');
|
|
|
|
const tx = meta.tx;
|
|
|
|
if (!verbose)
|
|
return tx.toHex();
|
|
|
|
let entry;
|
|
if (meta.block)
|
|
entry = await this.chain.getEntry(meta.block);
|
|
|
|
const json = this.txToJSON(tx, entry);
|
|
json.time = meta.mtime;
|
|
json.hex = tx.toHex();
|
|
|
|
return json;
|
|
}
|
|
|
|
async sendRawTransaction(args, help) {
|
|
if (help || args.length < 1 || args.length > 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'sendrawtransaction "hexstring" ( allowhighfees )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.');
|
|
|
|
const tx = TX.decode(data);
|
|
|
|
this.node.relay(tx);
|
|
|
|
return tx.txid();
|
|
}
|
|
|
|
async signRawTransaction(args, help) {
|
|
if (help || args.length < 1 || args.length > 4) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'signrawtransaction'
|
|
+ ' "hexstring" ('
|
|
+ ' [{"txid":"id","vout":n,"address":"bech32",'
|
|
+ 'redeemScript":"hex"},...] ["privatekey1",...]'
|
|
+ ' sighashtype )');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0);
|
|
const prevout = valid.array(1);
|
|
const secrets = valid.array(2);
|
|
const sighash = valid.str(3);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid hex string.');
|
|
|
|
if (!this.mempool)
|
|
throw new RPCError(errs.MISC_ERROR, 'No mempool available.');
|
|
|
|
const tx = MTX.decode(data);
|
|
tx.view = await this.mempool.getSpentView(tx);
|
|
|
|
const map = new BufferMap();
|
|
const keys = [];
|
|
|
|
if (secrets) {
|
|
const valid = new Validator(secrets);
|
|
for (let i = 0; i < secrets.length; i++) {
|
|
const secret = valid.str(i, '');
|
|
const key = parseSecret(secret, this.network);
|
|
map.set(key.getPublicKey(), key);
|
|
keys.push(key);
|
|
}
|
|
}
|
|
|
|
if (prevout) {
|
|
for (const prev of prevout) {
|
|
const valid = new Validator(prev);
|
|
const hash = valid.bhash('txid');
|
|
const index = valid.u32('vout');
|
|
const addrRaw = valid.str('address');
|
|
const value = valid.ufixed('amount', EXP);
|
|
const redeemRaw = valid.buf('redeemScript');
|
|
|
|
if (!hash || index == null || !addrRaw || value == null)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid UTXO.');
|
|
|
|
const outpoint = new Outpoint(hash, index);
|
|
|
|
const addr = parseAddress(addrRaw, this.network);
|
|
const coin = Output.fromScript(addr, value);
|
|
|
|
tx.view.addOutput(outpoint, coin);
|
|
|
|
if (keys.length === 0 || !redeemRaw)
|
|
continue;
|
|
|
|
if (!addr.isScripthash())
|
|
continue;
|
|
|
|
if (!redeemRaw) {
|
|
throw new RPCError(errs.INVALID_PARAMETER,
|
|
'P2SH requires redeem script.');
|
|
}
|
|
|
|
const redeem = Script.decode(redeemRaw);
|
|
|
|
for (const op of redeem.code) {
|
|
if (!op.data)
|
|
continue;
|
|
|
|
const key = map.get(op.data);
|
|
|
|
if (key) {
|
|
key.script = redeem;
|
|
key.refresh();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let type = Script.hashType.ALL;
|
|
if (sighash) {
|
|
const parts = sighash.split('|');
|
|
|
|
if (parts.length < 1 || parts.length > 2)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.');
|
|
|
|
type = Script.hashType[parts[0]];
|
|
|
|
if (type == null)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.');
|
|
|
|
if (parts.length === 2) {
|
|
if (parts[1] !== 'NOINPUT' && parts[1] !== 'ANYONECANPAY')
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid sighash type.');
|
|
|
|
if (parts[1] === 'NOINPUT')
|
|
type |= Script.hashType.NOINPUT;
|
|
|
|
if (parts[1] === 'ANYONECANPAY')
|
|
type |= Script.hashType.ANYONECANPAY;
|
|
}
|
|
}
|
|
|
|
await tx.signAsync(keys, type, this.workers);
|
|
|
|
return {
|
|
hex: tx.toHex(),
|
|
complete: tx.isSigned()
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Utility Functions
|
|
*/
|
|
|
|
async createMultisig(args, help) {
|
|
if (help || args.length < 2 || args.length > 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'createmultisig nrequired ["key",...]');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const keys = valid.array(1, []);
|
|
const m = valid.u32(0, 0);
|
|
const n = keys.length;
|
|
|
|
if (m < 1 || n < m || n > 16)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid m and n values.');
|
|
|
|
const items = new Validator(keys);
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = items.buf(i);
|
|
|
|
if (!key)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid key.');
|
|
|
|
if (!secp256k1.publicKeyVerify(key) || key.length !== 33)
|
|
throw new RPCError(errs.INVALID_ADDRESS_OR_KEY, 'Invalid key.');
|
|
|
|
keys[i] = key;
|
|
}
|
|
|
|
const script = Script.fromMultisig(m, n, keys);
|
|
|
|
if (script.getSize() > consensus.MAX_SCRIPT_PUSH) {
|
|
throw new RPCError(errs.VERIFY_ERROR,
|
|
'Redeem script exceeds size limit.');
|
|
}
|
|
|
|
const addr = Address.fromScripthash(script.sha3());
|
|
|
|
return {
|
|
address: addr.toString(this.network),
|
|
redeemScript: script.toJSON()
|
|
};
|
|
}
|
|
|
|
async validateAddress(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'validateaddress "address"');
|
|
|
|
const valid = new Validator(args);
|
|
const str = valid.str(0, '');
|
|
|
|
let addr;
|
|
try {
|
|
addr = Address.fromString(str, this.network);
|
|
} catch (e) {
|
|
return {
|
|
isvalid: false
|
|
};
|
|
}
|
|
|
|
return {
|
|
isvalid: true,
|
|
address: addr.toString(this.network),
|
|
isscript: addr.isScripthash(),
|
|
isspendable: !addr.isUnspendable(),
|
|
witness_version: addr.version,
|
|
witness_program: addr.hash.toString('hex')
|
|
};
|
|
}
|
|
|
|
async verifyMessage(args, help) {
|
|
if (help || args.length !== 3) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'verifymessage "address" "signature" "message"');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const b58 = valid.str(0, '');
|
|
const sig = valid.buf(1, null, 'base64');
|
|
const str = valid.str(2);
|
|
|
|
if (!sig || !str)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid parameters.');
|
|
|
|
const addr = parseAddress(b58, this.network);
|
|
|
|
if (addr.version !== 0 || addr.hash.length !== 20)
|
|
return false;
|
|
|
|
const msg = Buffer.from(MAGIC_STRING + str, 'utf8');
|
|
const hash = blake2b.digest(msg);
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
const key = secp256k1.recover(hash, sig, i, true);
|
|
|
|
if (!key)
|
|
continue;
|
|
|
|
if (safeEqual(blake2b.digest(key, 20), addr.hash))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async verifyMessageWithName(args, help) {
|
|
if (help || args.length < 3 || args.length > 4 ) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'verifymessagewithname "name" "signature" "message" (safe)');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const name = valid.str(0, '');
|
|
const sig = valid.buf(1, null, 'base64');
|
|
const str = valid.str(2);
|
|
const safe = valid.bool(3, false);
|
|
const network = this.network;
|
|
const height = this.chain.height;
|
|
|
|
if (!name || !rules.verifyName(name))
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid name.');
|
|
|
|
const nameHash = rules.hashName(name);
|
|
|
|
const ns = await this.getNameState(nameHash, safe);
|
|
|
|
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 this.chain.getCoin(ns.owner.hash, ns.owner.index);
|
|
|
|
if (!coin) {
|
|
throw new RPCError(
|
|
errs.DATABASE_ERROR,
|
|
'Cannot find the owner\'s address.'
|
|
);
|
|
}
|
|
|
|
const address = coin.address.toString(this.network);
|
|
return this.verifyMessage([address, sig, str]);
|
|
}
|
|
|
|
async signMessageWithPrivkey(args, help) {
|
|
if (help || args.length !== 2) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'signmessagewithprivkey "privkey" "message"');
|
|
}
|
|
|
|
const valid = new Validator(args);
|
|
const wif = valid.str(0, '');
|
|
const str = valid.str(1, '');
|
|
|
|
const key = parseSecret(wif, this.network);
|
|
const msg = Buffer.from(MAGIC_STRING + str, 'utf8');
|
|
const hash = blake2b.digest(msg);
|
|
const sig = key.sign(hash);
|
|
|
|
return sig.toString('base64');
|
|
}
|
|
|
|
async estimateFee(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'estimatefee nblocks');
|
|
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0, 1);
|
|
|
|
if (!this.fees)
|
|
throw new RPCError(errs.MISC_ERROR, 'Fee estimation not available.');
|
|
|
|
const fee = this.fees.estimateFee(blocks, false);
|
|
|
|
if (fee === 0)
|
|
return -1;
|
|
|
|
return Amount.coin(fee, true);
|
|
}
|
|
|
|
async estimatePriority(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'estimatepriority nblocks');
|
|
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0, 1);
|
|
|
|
if (!this.fees)
|
|
throw new RPCError(errs.MISC_ERROR, 'Priority estimation not available.');
|
|
|
|
return this.fees.estimatePriority(blocks, false);
|
|
}
|
|
|
|
async estimateSmartFee(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'estimatesmartfee nblocks');
|
|
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0, 1);
|
|
|
|
if (!this.fees)
|
|
throw new RPCError(errs.MISC_ERROR, 'Fee estimation not available.');
|
|
|
|
let fee = this.fees.estimateFee(blocks, true);
|
|
|
|
if (fee === 0)
|
|
fee = -1;
|
|
else
|
|
fee = Amount.coin(fee, true);
|
|
|
|
return {
|
|
fee: fee,
|
|
blocks: blocks
|
|
};
|
|
}
|
|
|
|
async estimateSmartPriority(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'estimatesmartpriority nblocks');
|
|
|
|
const valid = new Validator(args);
|
|
const blocks = valid.u32(0, 1);
|
|
|
|
if (!this.fees)
|
|
throw new RPCError(errs.MISC_ERROR, 'Priority estimation not available.');
|
|
|
|
const pri = this.fees.estimatePriority(blocks, true);
|
|
|
|
return {
|
|
priority: pri,
|
|
blocks: blocks
|
|
};
|
|
}
|
|
|
|
async invalidateBlock(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'invalidateblock "hash"');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.');
|
|
|
|
await this.chain.invalidate(hash);
|
|
|
|
return null;
|
|
}
|
|
|
|
async reconsiderBlock(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'reconsiderblock "hash"');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid block hash.');
|
|
|
|
this.chain.removeInvalid(hash);
|
|
|
|
return null;
|
|
}
|
|
|
|
async setMockTime(args, help) {
|
|
if (help || args.length !== 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'setmocktime timestamp');
|
|
|
|
const valid = new Validator(args);
|
|
const time = valid.u32(0);
|
|
|
|
if (time == null)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid timestamp.');
|
|
|
|
this.network.time.offset = 0;
|
|
|
|
const delta = this.network.now() - time;
|
|
|
|
this.network.time.offset = -delta;
|
|
|
|
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;
|
|
}
|
|
|
|
async getNames(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnames');
|
|
|
|
const network = this.network;
|
|
const height = this.chain.height;
|
|
const txn = this.chain.db.txn;
|
|
const items = [];
|
|
|
|
const iter = txn.iterator();
|
|
|
|
while (await iter.next()) {
|
|
const {key, value} = iter;
|
|
const ns = NameState.decode(value);
|
|
ns.nameHash = key;
|
|
|
|
const info = ns.getJSON(height, network);
|
|
items.push(info);
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
async getNameInfo(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnameinfo "name" (safe)');
|
|
|
|
const valid = new Validator(args);
|
|
const name = valid.str(0);
|
|
const safe = valid.bool(1, false);
|
|
|
|
if (!name || !rules.verifyName(name))
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid name.');
|
|
|
|
const network = this.network;
|
|
const height = this.chain.height;
|
|
const nameHash = rules.hashName(name);
|
|
const reserved = rules.isReserved(nameHash, height + 1, network);
|
|
const [start, week] = rules.getRollout(nameHash, network);
|
|
const state = await this.chain.getNextState();
|
|
|
|
const ns = await this.getNameState(nameHash, safe);
|
|
|
|
let locked = undefined;
|
|
let info = null;
|
|
|
|
if (ns) {
|
|
if (!ns.isExpired(height, network))
|
|
info = ns.getJSON(height, network);
|
|
}
|
|
|
|
if (state.hasICANNLockup())
|
|
locked = rules.isLockedUp(nameHash, height + 1, network);
|
|
|
|
return {
|
|
start: {
|
|
reserved: reserved,
|
|
week: week,
|
|
start: start,
|
|
locked: locked
|
|
},
|
|
info
|
|
};
|
|
}
|
|
|
|
async getNameResource(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnameresource "name" (safe)');
|
|
|
|
const valid = new Validator(args);
|
|
const name = valid.str(0);
|
|
const safe = valid.bool(1, false);
|
|
|
|
if (!name || !rules.verifyName(name))
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid name.');
|
|
|
|
const nameHash = rules.hashName(name);
|
|
|
|
const ns = await this.getNameState(nameHash, safe);
|
|
|
|
if (!ns || ns.data.length === 0)
|
|
return null;
|
|
|
|
try {
|
|
const res = Resource.decode(ns.data);
|
|
return res.getJSON(name);
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async getNameProof(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnameproof "name" ("root")');
|
|
|
|
const valid = new Validator(args);
|
|
const name = valid.str(0);
|
|
const treeRoot = valid.bhash(1);
|
|
|
|
if (!name || !rules.verifyName(name))
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid name.');
|
|
|
|
const hash = this.chain.tip.hash;
|
|
const height = this.chain.tip.height;
|
|
const root = treeRoot || this.chain.tip.treeRoot;
|
|
const key = rules.hashName(name);
|
|
const proof = await this.chain.db.prove(root, key);
|
|
|
|
return {
|
|
hash: hash.toString('hex'),
|
|
height: height,
|
|
root: root.toString('hex'),
|
|
name: name,
|
|
key: key.toString('hex'),
|
|
proof: proof.toJSON()
|
|
};
|
|
}
|
|
|
|
async getDNSSECProof(args, help) {
|
|
if (help || args.length < 1 || args.length > 3)
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'getdnssecproof "name" ( estimate ) ( verbose )');
|
|
|
|
const valid = new Validator(args);
|
|
const name = valid.str(0);
|
|
const estimate = valid.bool(1, false);
|
|
const verbose = valid.bool(2, true);
|
|
|
|
const proof = await ownership.prove(name, estimate);
|
|
|
|
if (!verbose)
|
|
return proof.toHex();
|
|
|
|
return proof.toJSON();
|
|
}
|
|
|
|
async getNameByHash(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'getnamebyhash "hash" (safe)');
|
|
|
|
const valid = new Validator(args);
|
|
const hash = valid.bhash(0);
|
|
const safe = valid.bool(1, false);
|
|
|
|
if (!hash)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid name hash.');
|
|
|
|
const ns = await this.getNameState(hash, safe);
|
|
|
|
if (!ns)
|
|
return null;
|
|
|
|
return ns.name.toString('binary');
|
|
}
|
|
|
|
async grindName(args, help) {
|
|
if (help || args.length > 1)
|
|
throw new RPCError(errs.MISC_ERROR, 'grindname size');
|
|
|
|
const valid = new Validator(args);
|
|
const size = valid.u32(0, 10);
|
|
|
|
if (size < 1 || size > 63)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid length.');
|
|
|
|
const network = this.network;
|
|
const height = this.chain.height;
|
|
|
|
return rules.grindName(size, height + 1, network);
|
|
}
|
|
|
|
async sendRawClaim(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'sendrawclaim "base64-string"');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0, null, 'base64');
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid base64 string.');
|
|
|
|
const claim = Claim.fromBlob(data);
|
|
|
|
this.node.relayClaim(claim);
|
|
|
|
return claim.hash().toString('hex');
|
|
}
|
|
|
|
async sendRawAirdrop(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'sendrawairdrop "base64-string"');
|
|
|
|
if (this.network.type !== 'main')
|
|
throw new RPCError(errs.MISC_ERROR, 'Currently disabled.');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.buf(0, null, 'base64');
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid base64 string.');
|
|
|
|
const proof = AirdropProof.decode(data);
|
|
|
|
this.node.relayAirdrop(proof);
|
|
|
|
return proof.hash().toString('hex');
|
|
}
|
|
|
|
async validateResource(args, help) {
|
|
if (help || args.length < 1 || args.length > 2)
|
|
throw new RPCError(errs.MISC_ERROR, 'validateresource'
|
|
+ ' \'{"records": [{...}]}\'');
|
|
|
|
const valid = new Validator(args);
|
|
const data = valid.obj(0);
|
|
|
|
if (!data)
|
|
throw new RPCError(errs.TYPE_ERROR, 'Invalid resource object.');
|
|
|
|
let resource;
|
|
try {
|
|
resource = Resource.fromJSON(data);
|
|
} catch (e) {
|
|
throw new RPCError(errs.PARSE_ERROR, e.message);
|
|
}
|
|
|
|
return resource.toJSON();
|
|
}
|
|
|
|
async resetRootCache(args, help) {
|
|
if (help || args.length !== 0)
|
|
throw new RPCError(errs.MISC_ERROR, 'resetrootcache');
|
|
|
|
if (!this.node.ns)
|
|
return null;
|
|
|
|
this.node.ns.resetCache();
|
|
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
async handleLongpoll(lpid) {
|
|
if (lpid.length !== 72)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.');
|
|
|
|
const watched = lpid.slice(0, 64);
|
|
const lastTX = parseInt(lpid.slice(64, 72), 16);
|
|
|
|
if ((lastTX >>> 0) !== lastTX)
|
|
throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.');
|
|
|
|
const hash = util.parseHex(watched, 32);
|
|
|
|
if (!this.chain.tip.hash.equals(hash))
|
|
return;
|
|
|
|
await this.longpoll();
|
|
}
|
|
|
|
longpoll() {
|
|
return new Promise((resolve, reject) => {
|
|
this.pollers.push({ resolve, reject });
|
|
});
|
|
}
|
|
|
|
refreshBlock() {
|
|
const pollers = this.pollers;
|
|
|
|
this.attempt = null;
|
|
this.lastActivity = 0;
|
|
this.pollers = [];
|
|
|
|
for (const job of pollers)
|
|
job.resolve();
|
|
}
|
|
|
|
bindChain() {
|
|
if (this.boundChain)
|
|
return;
|
|
|
|
this.boundChain = true;
|
|
|
|
const refresh = () => {
|
|
if (!this.attempt)
|
|
return;
|
|
|
|
this.refreshBlock();
|
|
this.merkleMap.clear();
|
|
this.merkleList.length = 0;
|
|
};
|
|
|
|
this.node.on('connect', refresh);
|
|
this.node.on('reset', refresh);
|
|
|
|
if (!this.mempool)
|
|
return;
|
|
|
|
const tryRefresh = () => {
|
|
if (!this.attempt)
|
|
return;
|
|
|
|
if (util.now() - this.lastActivity > 10)
|
|
this.refreshBlock();
|
|
};
|
|
|
|
this.node.on('tx', tryRefresh);
|
|
this.node.on('claim', tryRefresh);
|
|
this.node.on('airdrop', tryRefresh);
|
|
}
|
|
|
|
async getTemplate() {
|
|
this.bindChain();
|
|
|
|
let attempt = this.attempt;
|
|
|
|
if (attempt) {
|
|
this.miner.updateTime(attempt);
|
|
} else {
|
|
attempt = await this.miner.createBlock();
|
|
this.attempt = attempt;
|
|
this.lastActivity = util.now();
|
|
}
|
|
|
|
return attempt;
|
|
}
|
|
|
|
async updateWork() {
|
|
this.bindChain();
|
|
|
|
let attempt = this.attempt;
|
|
|
|
if (attempt) {
|
|
if (attempt.address.isNull()) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'No addresses available for coinbase.');
|
|
}
|
|
|
|
this.miner.updateTime(attempt);
|
|
|
|
return attempt;
|
|
}
|
|
|
|
if (this.miner.addresses.length === 0) {
|
|
throw new RPCError(errs.MISC_ERROR,
|
|
'No addresses available for coinbase.');
|
|
}
|
|
|
|
attempt = await this.miner.createBlock();
|
|
|
|
if (this.merkleMap.size >= 10)
|
|
this.merkleMap.delete(this.merkleList.shift());
|
|
|
|
this.attempt = attempt;
|
|
this.lastActivity = util.now();
|
|
this.merkleMap.set(attempt.witnessRoot, attempt);
|
|
this.merkleList.push(attempt.witnessRoot);
|
|
|
|
return attempt;
|
|
}
|
|
|
|
async addBlock(block) {
|
|
const unlock1 = await this.locker.lock();
|
|
const unlock2 = await this.chain.locker.lock();
|
|
try {
|
|
return await this._addBlock(block);
|
|
} finally {
|
|
unlock2();
|
|
unlock1();
|
|
}
|
|
}
|
|
|
|
async _addBlock(block) {
|
|
this.logger.info('Handling submitted block: %x.', block.hash());
|
|
|
|
let entry;
|
|
try {
|
|
entry = await this.chain._add(block);
|
|
} catch (err) {
|
|
if (err.type === 'VerifyError') {
|
|
this.logger.warning('RPC block rejected: %x (%s).',
|
|
block.hash(), err.reason);
|
|
return `rejected: ${err.reason}`;
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
if (!entry) {
|
|
this.logger.warning('RPC block rejected: %x (bad-prevblk).',
|
|
block.hash());
|
|
return 'rejected: bad-prevblk';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
totalTX() {
|
|
return this.mempool ? this.mempool.map.size : 0;
|
|
}
|
|
|
|
async getSoftforks() {
|
|
const tip = this.chain.tip;
|
|
const forks = {};
|
|
|
|
for (const deployment of this.network.deploys) {
|
|
const state = await this.chain.getState(tip, deployment);
|
|
let status;
|
|
|
|
switch (state) {
|
|
case common.thresholdStates.DEFINED:
|
|
status = 'defined';
|
|
break;
|
|
case common.thresholdStates.STARTED:
|
|
status = 'started';
|
|
break;
|
|
case common.thresholdStates.LOCKED_IN:
|
|
status = 'locked_in';
|
|
break;
|
|
case common.thresholdStates.ACTIVE:
|
|
status = 'active';
|
|
break;
|
|
case common.thresholdStates.FAILED:
|
|
status = 'failed';
|
|
break;
|
|
default:
|
|
assert(false, 'Bad state.');
|
|
break;
|
|
}
|
|
|
|
forks[deployment.name] = {
|
|
status: status,
|
|
bit: deployment.bit,
|
|
startTime: deployment.startTime,
|
|
timeout: deployment.timeout
|
|
};
|
|
|
|
if (status === 'started') {
|
|
forks[deployment.name].statistics =
|
|
await this.chain.getBIP9Stats(tip, deployment);
|
|
}
|
|
}
|
|
|
|
return forks;
|
|
}
|
|
|
|
async getHashRate(lookup, height) {
|
|
let tip = this.chain.tip;
|
|
|
|
if (height != null)
|
|
tip = await this.chain.getEntry(height);
|
|
|
|
if (!tip)
|
|
return 0;
|
|
|
|
assert(typeof lookup === 'number');
|
|
assert(lookup >= 0);
|
|
|
|
if (lookup === 0)
|
|
lookup = tip.height % this.network.pow.targetWindow + 1;
|
|
|
|
if (lookup > tip.height)
|
|
lookup = tip.height;
|
|
|
|
let min = tip.time;
|
|
let max = min;
|
|
let entry = tip;
|
|
|
|
for (let i = 0; i < lookup; i++) {
|
|
entry = await this.chain.getPrevious(entry);
|
|
|
|
if (!entry)
|
|
throw new RPCError(errs.DATABASE_ERROR, 'Not found.');
|
|
|
|
min = Math.min(entry.time, min);
|
|
max = Math.max(entry.time, max);
|
|
}
|
|
|
|
const diff = max - min;
|
|
|
|
if (diff === 0)
|
|
return 0;
|
|
|
|
const work = tip.chainwork.sub(entry.chainwork);
|
|
|
|
return Number(work.toString()) / diff;
|
|
}
|
|
|
|
async mineBlocks(blocks, addr, tries) {
|
|
const unlock = await this.locker.lock();
|
|
try {
|
|
return await this._mineBlocks(blocks, addr, tries);
|
|
} finally {
|
|
unlock();
|
|
}
|
|
}
|
|
|
|
async _mineBlocks(blocks, addr, tries) {
|
|
const hashes = [];
|
|
|
|
for (let i = 0; i < blocks; i++) {
|
|
const block = await this.miner.mineBlock(null, addr);
|
|
const entry = await this.chain.add(block);
|
|
assert(entry);
|
|
hashes.push(entry.hash.toString('hex'));
|
|
}
|
|
|
|
return hashes;
|
|
}
|
|
|
|
async findFork(entry) {
|
|
while (entry) {
|
|
if (await this.chain.isMainChain(entry))
|
|
return entry;
|
|
entry = await this.chain.getPrevious(entry);
|
|
}
|
|
throw new Error('Fork not found.');
|
|
}
|
|
|
|
txToJSON(tx, entry) {
|
|
let height = -1;
|
|
let time = 0;
|
|
let hash = null;
|
|
let conf = 0;
|
|
|
|
if (entry) {
|
|
height = entry.height;
|
|
time = entry.time;
|
|
hash = entry.hash;
|
|
conf = this.chain.height - height + 1;
|
|
}
|
|
|
|
const vin = [];
|
|
|
|
for (const input of tx.inputs) {
|
|
const json = {
|
|
coinbase: undefined,
|
|
txid: undefined,
|
|
vout: undefined,
|
|
txinwitness: undefined,
|
|
sequence: input.sequence,
|
|
link: input.link
|
|
};
|
|
|
|
json.coinbase = tx.isCoinbase();
|
|
json.txid = input.prevout.txid();
|
|
json.vout = input.prevout.index;
|
|
json.txinwitness = input.witness.toJSON();
|
|
|
|
vin.push(json);
|
|
}
|
|
|
|
const vout = [];
|
|
|
|
for (let i = 0; i < tx.outputs.length; i++) {
|
|
const output = tx.outputs[i];
|
|
vout.push({
|
|
value: Amount.coin(output.value, true),
|
|
n: i,
|
|
address: this.addrToJSON(output.address),
|
|
covenant: output.covenant.toJSON()
|
|
});
|
|
}
|
|
|
|
return {
|
|
txid: tx.txid(),
|
|
hash: tx.wtxid(),
|
|
size: tx.getSize(),
|
|
vsize: tx.getVirtualSize(),
|
|
version: tx.version,
|
|
locktime: tx.locktime,
|
|
vin: vin,
|
|
vout: vout,
|
|
blockhash: hash ? hash.toString('hex') : null,
|
|
confirmations: conf,
|
|
time: time,
|
|
blocktime: time,
|
|
hex: undefined
|
|
};
|
|
}
|
|
|
|
scriptToJSON(script, hex) {
|
|
const type = script.getType();
|
|
|
|
const json = {
|
|
asm: script.toASM(),
|
|
hex: undefined,
|
|
type: Script.typesByVal[type],
|
|
reqSigs: 1,
|
|
totalSigs: 1,
|
|
p2sh: undefined
|
|
};
|
|
|
|
if (hex)
|
|
json.hex = script.toJSON();
|
|
|
|
const [m, n] = script.getMultisig();
|
|
|
|
if (m !== -1)
|
|
json.reqSigs = m;
|
|
|
|
if (n !== -1)
|
|
json.totalSigs = n;
|
|
|
|
return json;
|
|
}
|
|
|
|
addrToJSON(addr) {
|
|
return {
|
|
version: addr.version,
|
|
hash: addr.hash.toString('hex'),
|
|
string: addr.toString(this.network)
|
|
};
|
|
}
|
|
|
|
async headerToJSON(entry) {
|
|
const mtp = await this.chain.getMedianTime(entry);
|
|
const next = await this.chain.getNextHash(entry.hash);
|
|
|
|
let confirmations = -1;
|
|
if (await this.chain.isMainChain(entry))
|
|
confirmations = this.chain.height - entry.height + 1;
|
|
|
|
return {
|
|
hash: entry.hash.toString('hex'),
|
|
confirmations: confirmations,
|
|
height: entry.height,
|
|
version: entry.version,
|
|
versionHex: util.hex32(entry.version),
|
|
merkleroot: entry.merkleRoot.toString('hex'),
|
|
witnessroot: entry.witnessRoot.toString('hex'),
|
|
treeroot: entry.treeRoot.toString('hex'),
|
|
reservedroot: entry.reservedRoot.toString('hex'),
|
|
mask: entry.mask.toString('hex'),
|
|
time: entry.time,
|
|
mediantime: mtp,
|
|
nonce: entry.nonce,
|
|
extranonce: entry.extraNonce.toString('hex'),
|
|
bits: util.hex32(entry.bits),
|
|
difficulty: toDifficulty(entry.bits),
|
|
chainwork: entry.chainwork.toString('hex', 64),
|
|
previousblockhash: !entry.prevBlock.equals(consensus.ZERO_HASH)
|
|
? entry.prevBlock.toString('hex')
|
|
: null,
|
|
nextblockhash: next ? next.toString('hex') : null
|
|
};
|
|
}
|
|
|
|
async blockToJSON(entry, block, details) {
|
|
const mtp = await this.chain.getMedianTime(entry);
|
|
const next = await this.chain.getNextHash(entry.hash);
|
|
|
|
let confirmations = -1;
|
|
if (await this.chain.isMainChain(entry))
|
|
confirmations = this.chain.height - entry.height + 1;
|
|
|
|
const txs = [];
|
|
|
|
for (const tx of block.txs) {
|
|
if (details) {
|
|
const json = this.txToJSON(tx, entry);
|
|
txs.push(json);
|
|
continue;
|
|
}
|
|
txs.push(tx.txid());
|
|
}
|
|
|
|
return {
|
|
hash: entry.hash.toString('hex'),
|
|
confirmations: confirmations,
|
|
strippedsize: block.getBaseSize(),
|
|
size: block.getSize(),
|
|
weight: block.getWeight(),
|
|
height: entry.height,
|
|
version: entry.version,
|
|
versionHex: util.hex32(entry.version),
|
|
merkleroot: entry.merkleRoot.toString('hex'),
|
|
witnessroot: entry.witnessRoot.toString('hex'),
|
|
treeroot: entry.treeRoot.toString('hex'),
|
|
reservedroot: entry.reservedRoot.toString('hex'),
|
|
mask: entry.mask.toString('hex'),
|
|
coinbase: !details
|
|
? block.txs[0].inputs[0].witness.toJSON()
|
|
: undefined,
|
|
tx: txs,
|
|
time: entry.time,
|
|
mediantime: mtp,
|
|
nonce: entry.nonce,
|
|
extranonce: entry.extraNonce.toString('hex'),
|
|
bits: util.hex32(entry.bits),
|
|
difficulty: toDifficulty(entry.bits),
|
|
chainwork: entry.chainwork.toString('hex', 64),
|
|
nTx: txs.length,
|
|
previousblockhash: !entry.prevBlock.equals(consensus.ZERO_HASH)
|
|
? entry.prevBlock.toString('hex')
|
|
: null,
|
|
nextblockhash: next ? next.toString('hex') : null
|
|
};
|
|
}
|
|
|
|
entryToJSON(entry) {
|
|
return {
|
|
size: entry.size,
|
|
fee: Amount.coin(entry.deltaFee, true),
|
|
modifiedfee: 0,
|
|
time: entry.time,
|
|
height: entry.height,
|
|
startingpriority: entry.priority,
|
|
currentpriority: entry.getPriority(this.chain.height),
|
|
descendantcount: this.mempool.countDescendants(entry),
|
|
descendantsize: entry.descSize,
|
|
descendantfees: entry.descFee,
|
|
ancestorcount: this.mempool.countAncestors(entry),
|
|
ancestorsize: 0,
|
|
ancestorfees: 0,
|
|
depends: this.mempool.getDepends(entry.tx)
|
|
};
|
|
}
|
|
|
|
async getNameState(nameHash, safe) {
|
|
if (!safe) {
|
|
// Will always return null in SPV mode
|
|
return this.chain.db.getNameState(nameHash);
|
|
}
|
|
|
|
// Safe roots are the last Urkel tree commitment
|
|
// with more than 12 confirmations.
|
|
const root = await this.chain.getSafeRoot();
|
|
let data;
|
|
if (this.chain.options.spv)
|
|
data = await this.pool.resolveAtRoot(nameHash, root);
|
|
else
|
|
data = await this.chain.db.lookup(root, nameHash);
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
const ns = NameState.decode(data);
|
|
ns.nameHash = nameHash;
|
|
return ns;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
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.');
|
|
}
|
|
}
|
|
|
|
function parseIP(addr, network) {
|
|
let ip;
|
|
|
|
try {
|
|
ip = IP.fromHostname(addr);
|
|
} catch (e) {
|
|
throw new RPCError(errs.CLIENT_INVALID_IP_OR_SUBNET,
|
|
'Invalid IP address or subnet.');
|
|
}
|
|
|
|
if (ip.port === 0)
|
|
ip.port = ip.key ? network.brontidePort : network.port;
|
|
|
|
return ip;
|
|
}
|
|
|
|
function parseNetAddress(addr, network) {
|
|
try {
|
|
return NetAddress.fromHostname(addr, network);
|
|
} catch (e) {
|
|
throw new RPCError(errs.CLIENT_INVALID_IP_OR_SUBNET,
|
|
'Invalid IP address or subnet.');
|
|
}
|
|
}
|
|
|
|
function toDifficulty(bits) {
|
|
let shift = (bits >>> 24) & 0xff;
|
|
let diff = 0x0000ffff / (bits & 0x00ffffff);
|
|
|
|
while (shift < 29) {
|
|
diff *= 256.0;
|
|
shift++;
|
|
}
|
|
|
|
while (shift > 29) {
|
|
diff /= 256.0;
|
|
shift--;
|
|
}
|
|
|
|
return diff;
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = RPC;
|