Source

tools/rpc_service.js

/*jslint node: true */
/**
 * @namespace rpc_service
 */
/*
	Accept commands via JSON-RPC API.
	The daemon listens on port 6332 by default.
	See https://developer.obyte.org/json-rpc/running-rpc-service for detailed description of the API
*/

"use strict";
var fs = require('fs');
var desktopApp = require('ocore/desktop_app.js');
var appDataDir = desktopApp.getAppDataDir();
var path = require('path');

if (require.main === module && !fs.existsSync(appDataDir) && fs.existsSync(path.dirname(appDataDir)+'/headless-byteball')){
	console.log('=== will rename old data dir');
	fs.renameSync(path.dirname(appDataDir)+'/headless-byteball', appDataDir);
}
var conf = require('ocore/conf.js');
if (!conf.rpcPort)
	throw new Error('conf.rpcPort must be configured.');

var headlessWallet = require('../start.js');
var eventBus = require('ocore/event_bus.js');
var db = require('ocore/db.js');
var mutex = require('ocore/mutex.js');
var storage = require('ocore/storage.js');
var constants = require('ocore/constants.js');
var validationUtils = require("ocore/validation_utils.js");
var wallet_id;

function initRPC() {
	var network = require('ocore/network.js');

	var rpc = require('json-rpc2');
	var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js');
	var Wallet = require('ocore/wallet.js');
	var balances = require('ocore/balances.js');

	var server = rpc.Server.$create({
		'websocket': true, // is true by default 
		'headers': { // allow custom headers is empty by default 
			'Access-Control-Allow-Origin': '*'
		}
	});

	/**
	 * @typedef {Object} getInfoResponse
	 * @property {number} connections
	 * @property {number} last_mci
	 * @property {number} last_stable_mci
	 * @property {number} count_unhandled
	 */
	/**
	 * Returns information about the current state.
	 * @name getInfo
	 * @memberOf rpc_service
	 * @function
	 * @returns {getInfoResponse} Response
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getinfo", "params":{} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getinfo', function(args, opt, cb) {
		var connections = network.getConnectionStatus();
		var response = {connections: connections.incoming+connections.outgoing};
		storage.readLastMainChainIndex(function(last_mci){
			response.last_mci = last_mci;
			storage.readLastStableMcIndex(db, function(last_stable_mci){
				response.last_stable_mci = last_stable_mci;
				db.query("SELECT COUNT(*) AS count_unhandled FROM unhandled_joints", function(rows){
					response.count_unhandled = rows[0].count_unhandled;
					cb(null, response);
				});
			});
		});
	});

	/**
	 * Returns the number of connections to other nodes.
	 * @name getConnectionCount
	 * @memberOf rpc_service
	 * @function
	 * @return {number} response
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getconnectioncount", "params":{} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getconnectioncount', function(args, opt, cb) {
		var connections = network.getConnectionStatus();
		cb(null, connections.incoming+connections.outgoing);
	});

	/**
	 * @typedef {Object} getNetworkInfoResponse
	 * @property {string} version
	 * @property {string} subversion
	 * @property {string} protocolversion
	 * @property {string} alt
	 * @property {number} connections
	 * @property {boolean} bLight
	 * @property {boolean} socksConfigured
	 * @property {number} COUNT_WITNESSES
	 * @property {number} MAJORITY_OF_WITNESSES
	 * @property {string} GENESIS_UNIT
	 * @property {string} BLACKBYTES_ASSET
	 */
	/**
	 * Returns information about the node's connection to the network.
	 * @name getNetworkInfo
	 * @memberOf rpc_service
	 * @function
	 * @return {getNetworkInfoResponse} Response
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getnetworkinfo", "params":{} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getnetworkinfo', function(args, opt, cb) {
		var connections = network.getConnectionStatus();
		cb(null, {
			"version": constants.minCoreVersion,
			"subversion": conf.program +' '+ conf.program_version,
			"protocolversion": constants.version,
			"alt": constants.alt,
			"connections": connections.incoming+connections.outgoing,
			"bLight": conf.bLight,
			"socksConfigured": !(!conf.socksHost || !conf.socksPort),
			"COUNT_WITNESSES": constants.COUNT_WITNESSES,
			"MAJORITY_OF_WITNESSES": constants.MAJORITY_OF_WITNESSES,
			"GENESIS_UNIT": constants.GENESIS_UNIT,
			"BLACKBYTES_ASSET": constants.BLACKBYTES_ASSET,
		});
	});

	/**
	 * Validates address.
	 * @name validateAddress
	 * @memberOf rpc_service
	 * @function
	 * @param {string} address
	 * @return {boolean} is_valid
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"validateaddress", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('validateaddress', validateaddres);
	// alias for validateaddress
	server.expose('verifyaddress', validateaddres);

	function validateaddres(args, opt, cb) {
		var address = Array.isArray(args) ? args[0] : args.address;
		cb(null, validationUtils.isValidAddress(address));
	}

	/**
	 * Creates and returns new wallet address.
	 * @name getNewAddress
	 * @memberOf rpc_service
	 * @function
	 * @return {string} address
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getnewaddress", "params":{} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getnewaddress', function(args, opt, cb) {
		mutex.lock(['rpc_getnewaddress'], function(unlock){
			walletDefinedByKeys.issueNextAddress(wallet_id, 0, function(addressInfo) {
				unlock();
				cb(null, addressInfo.address);
			});
		});
	});

	/**
	 * @typedef {Object} getaddressesResponse
	 * @property {string} address
	 * @property {string} address_index
	 * @property {number} is_change
	 * @property {number} is_definition_public
	 * @property {string} creation_ts
	 */
	/**
	 * Returns the list of addresses for the whole wallet.
	 * @name getAddresses
	 * @memberOf rpc_service
	 * @function
	 * @param {string} [type] - must be: "deposit", "change", "shared", "textcoin", null - shows both deposit and change by default
	 * @param {string|boolean} [reverse] - "reverse" by default
	 * @param {number|string} [limit] - 100 by default
	 * @param {string|boolean} [verbose] - off by default, includes is_definition_public info when "verbose"
	 * @return {Array<getaddressesResponse>} list of addresses
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getaddresses", "params":{} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getaddresses', function(args, opt, cb) {
		console.log('getaddresses '+JSON.stringify(args));
		let start_time = Date.now();
		var {type, reverse, limit, verbose} = args;
		if (Array.isArray(args))
			[type, reverse, limit, verbose] = args;
		reverse = (reverse == null || reverse === 'reverse') || String(reverse).toLowerCase() === "true";
		limit = parseInt(limit) || 100;
		verbose = (verbose === 'verbose') || String(verbose).toLowerCase() === "true";

		var sql;
		switch (type) {
			case 'textcoin':
				sql = "SELECT address, NULL AS address_index, NULL AS is_change";
				sql += verbose ? ", (CASE WHEN unit_authors.unit IS NULL THEN 0 ELSE 1 END) AS is_definition_public" : "";
				sql += ", "+ db.getUnixTimestamp("creation_date")+" AS creation_ts FROM sent_mnemonics";
				sql += verbose ? " LEFT JOIN unit_authors USING(address)" : "";
				sql += verbose ? " GROUP BY address" : "";
				break;
			case 'shared':
				sql = "SELECT shared_address AS address, NULL AS address_index, NULL AS is_change";
				sql += verbose ? ", (CASE WHEN unit_authors.unit IS NULL THEN 0 ELSE 1 END) AS is_definition_public" : "";
				sql += ", "+ db.getUnixTimestamp("creation_date")+" AS creation_ts FROM shared_addresses";
				sql += verbose ? " LEFT JOIN unit_authors ON shared_address = address" : "";
				sql += verbose ? " GROUP BY shared_address" : "";
				break;
			default:
				sql = "SELECT address, address_index, is_change";
				sql += verbose ? ", (CASE WHEN unit_authors.unit IS NULL THEN 0 ELSE 1 END) AS is_definition_public" : "";
				sql += ", "+ db.getUnixTimestamp("creation_date")+" AS creation_ts FROM my_addresses";
				sql += verbose ? " LEFT JOIN unit_authors USING(address)" : "";
				if (type === 'deposit' || type === 'change')
					sql += " WHERE is_change="+ (type === 'change' ? "1" : "0");
				sql += verbose ? " GROUP BY address" : "";
				break;
		}
		sql += " ORDER BY creation_ts "+ (reverse ? "DESC" : "") +" LIMIT "+ limit;
		db.query(sql, [], function(listOfAddresses) {
			console.log('getaddresses took '+(Date.now()-start_time)+'ms');
			cb(null, listOfAddresses);
		});
	});

	/**
	 * @typedef {Object} assetInBalanceResponse
	 * @property {number} stable
	 * @property {number} pending
	 */
	/**
	 * @typedef {Object} balanceResponse
	 * @property {assetInBalanceResponse} asset
	 */
	/**
	 * Returns address balance(stable and pending).<br>
	 * If address is invalid, then returns "invalid address".<br>
	 * If your wallet doesn`t own the address, then returns "address not found".<br>
	 * If no address supplied, returns wallet balance(stable and pending).
	 * @name getBalance
	 * @memberOf rpc_service
	 * @function
	 * @param {string} [address]
	 * @param {string} [asset]
	 * @return {balanceResponse} balance
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getbalance", "params":{} }' http://127.0.0.1:6332 | json_pp
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getbalance", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getbalance', function(args, opt, cb) {
		console.log('getbalance '+JSON.stringify(args));
		let start_time = Date.now();
		var {address, asset} = args;
		if (Array.isArray(args))
			[address, asset] = args;
		if (address) {
			if (!validationUtils.isValidAddress(address))
				return cb("invalid address");
			db.query("SELECT address FROM my_addresses WHERE address = ? UNION SELECT shared_address AS address FROM shared_addresses WHERE shared_address = ? UNION SELECT address FROM sent_mnemonics WHERE address = ?;", [address, address, address], function(rows) {
				if (rows.length !== 1)
					return cb("address not found");
				if (asset && asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH))
					return cb("bad asset: "+asset);
				db.query(
					"SELECT asset, is_stable, SUM(amount) AS balance \n\
					FROM outputs JOIN units USING(unit) \n\
					WHERE is_spent=0 AND address=? AND sequence='good' AND asset "+((asset && asset !== 'base') ? "="+db.escape(asset) : "IS NULL")+" \n\
					GROUP BY is_stable", [address],
					function(rows) {
						var balance = {};
						balance[asset || 'base'] = {
							stable: 0,
							pending: 0
						};
						for (var i = 0; i < rows.length; i++) {
							var row = rows[i];
							balance[asset || 'base'][row.is_stable ? 'stable' : 'pending'] = row.balance;
						}
						cb(null, balance);
					}
				);
			});
		}
		else
			Wallet.readBalance(wallet_id, function(balances) {
				console.log('getbalance took '+(Date.now()-start_time)+'ms');
				cb(null, balances);
			});
	});

	/**
	 * Returns wallet balance(stable and pending) without commissions earned from headers and witnessing.
	 * @name getMainBalance
	 * @memberOf rpc_service
	 * @function
	 * @return {balanceResponse} balance
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getmainbalance", "params":{} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('getmainbalance', function(args, opt, cb) {
		let start_time = Date.now();
		balances.readOutputsBalance(wallet_id, function(balances) {
			console.log('getmainbalance took '+(Date.now()-start_time)+'ms');
			cb(null, balances);
		});
	});

	/**
	 * @typedef {Object} gettransactionResponse
	 * @property {string} action
	 * @property {number} amount
	 * @property {string} my_address
	 * @property {Array<String>} arrPayerAddresses
	 * @property {number} confirmations
	 * @property {string} unit
	 * @property {number} fee
	 * @property {string} time
	 * @property {number} level
	 * @property {string} asset
	 */
	/**
	 * Returns transaction by unit ID.
	 * @name getTransaction
	 * @memberOf rpc_service
	 * @function
	 * @param {string} unit - transaction unit ID
	 * @param {boolean|string} [verbose] - includes unit definition if "verbose" is second parameter
	 * @param {string} [asset] - asset ID
	 * @return {gettransactionResponse} Response
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"gettransaction", "params":["vuudtbL5ASwr0LJZ9tuV4S0j/lIsotJCKifphvGATmU=", true] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('gettransaction', function(args, opt, cb) {
		var {unit, verbose, asset} = args;
		if (Array.isArray(args)) {
			if (typeof args[0] === 'string')
				[unit, verbose, asset] = args;
			else
				return cb('unit must be a string');
		}
		if (!unit)
			return cb('unit is required');
		verbose = (verbose === 'verbose') || String(verbose).toLowerCase() === "true";

		listtransactions({unit, since_mci:1, asset}, opt, function(err, results) {
			if (err)
				return cb(err);
			if (!results.length)
				return cb('transaction not found in wallet for ' + (asset || 'base') + ' asset');
			if (!verbose)
				return cb(null, {unit, details:results});
			storage.readJoint(db, unit, {
				ifFound: function(objJoint){
					cb(null, {unit, details:results, decoded:objJoint});
				},
				ifNotFound: function(){
					cb(null, {unit, details:results, decoded:null});
				}
			});
		});
	});

	/**
	 * Returns transaction list.<br>
	 * If address is invalid, then returns "invalid address".<br>
	 * If no address supplied, returns wallet transaction list.
	 * @name listTransactions
	 * @memberOf rpc_service
	 * @function
	 * @param {string} [address] - optional
	 * @param {string} [since_mci] - optional, counts only if no address
	 * @param {string} [unit] - optional, counts only if no address
	 * @param {string} [asset] - optional, counts only if no address
	 * @return {Array<gettransactionResponse>} Response
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"listtransactions", "params":{"since_mci": 1234} }' http://127.0.0.1:6332 | json_pp
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"listtransactions", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('listtransactions', listtransactions);

	function listtransactions(args, opt, cb) {
		console.log('listtransactions '+JSON.stringify(args));
		let start_time = Date.now();
		var {address, since_mci, unit, asset} = args;
		if (Array.isArray(args))
			[address, since_mci, unit, asset] = args;
		if (address) {
			if (!validationUtils.isValidAddress(address))
				return cb("invalid address");
			Wallet.readTransactionHistory({address: address}, function(result) {
				cb(null, result);
			});
		}
		else{
			var opts = {wallet: wallet_id};
			if (unit) {
				if (!validationUtils.isValidBase64(unit, constants.HASH_LENGTH))
					return cb('invalid unit');
				opts.unit = unit;
			}
			if (since_mci) {
				if (!validationUtils.isNonnegativeInteger(since_mci))
					return cb('invalid since_mci');
				opts.since_mci = since_mci;
			}
			else
				opts.limit = 200;
			if (asset){
				if (asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH))
					return cb("bad asset: "+asset);
				opts.asset = asset;
			}
			Wallet.readTransactionHistory(opts, function(result) {
				console.log('listtransactions '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms');
				cb(null, result);
			});
		}

	}

	/**
	 * Send funds to address.<br>
	 * If address is invalid, then returns "invalid address".
	 * @name sendToAddress
	 * @memberOf rpc_service
	 * @function
	 * @param {string} address - wallet address
	 * @param {number|string} amount - positive integer
	 * @param {string} [asset] - asset ID
	 * @returns {string} unit ID
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"sendtoaddress", "params":["BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3", 1000] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('sendtoaddress', function(args, opt, cb) {
		console.log('sendtoaddress '+JSON.stringify(args));
		let start_time = Date.now();
		var {address, amount, asset} = args;
		if (Array.isArray(args)) {
			if (typeof args[0] === 'string')
				[address, amount, asset] = args;
			else
				return cb('address must be a string');
		}
		if (amount != parseInt(amount) || parseInt(amount) < 1)
			return cb('amount must be positive integer');
		amount = parseInt(amount);
		if (asset && asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH))
			return cb("bad asset: "+asset);
		if (!amount || !address)
			return cb("required parameters missing");
		if (!validationUtils.isValidAddress(address))
			return cb("invalid address");

		headlessWallet.issueChangeAddressAndSendPayment(asset, amount, address, null, function(err, unit) {
			console.log('sendtoaddress '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err);
			cb(err, err ? undefined : unit);
		});
	});

	/**
	 * Send funds from address to address, keeping change to sending address.<br>
	 * If eiher addresses are invalid, then returns "invalid address" error.<br>
	 * If your wallet doesn`t own the address, then returns "address not found".<br>
	 * Bytes payment can have amount as 'all', other assets must specify exact amount.
	 * @name sendFrom
	 * @memberOf rpc_service
	 * @function
	 * @param {string} from_address - wallet address
	 * @param {string} to_address - wallet address
	 * @param {number|string} amount - positive integer or 'all' (for Bytes only)
	 * @param {string} [asset] - asset ID
	 * @return {string} unit ID
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"sendfrom", "params":{"from_address":"BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3", "to_address":"SNYRRHTIWDVJHSKE5BUIS3HWXKBN57JJ", "amount:"1000} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('sendfrom', function(args, opt, cb) {
		console.log('sendfrom '+JSON.stringify(args));
		let start_time = Date.now();
		var {from_address, to_address, amount, asset} = args;
		if (Array.isArray(args)) {
			if (typeof args[0] === 'string' && typeof args[1] === 'string')
				[from_address, to_address, amount, asset] = args;
			else
				return cb('from_address and to_address must be strings');
		}
		amount = (String(amount).toLowerCase() === 'all') ? 'all' : amount;
		if (amount !== 'all') {
			if (amount != parseInt(amount) || parseInt(amount) < 1)
				return cb('amount must be positive integer');
			amount = parseInt(amount);
		}
		if (asset && asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH))
			return cb("bad asset: "+asset);
		if (!amount || !to_address || !from_address)
			return cb("required parameters missing");
		if (!validationUtils.isValidAddress(to_address) || !validationUtils.isValidAddress(from_address))
			return cb("invalid address");

		db.query("SELECT address FROM my_addresses WHERE address = ? UNION SELECT shared_address AS address FROM shared_addresses WHERE shared_address = ?;", [from_address, from_address], function(rows){
			if (rows.length !== 1)
				return cb("address not found");
			if (amount === 'all') {
				if (asset && asset !== 'base')
					return cb("use exact amount for custom assets");

				headlessWallet.sendAllBytesFromAddress(from_address, to_address, null, function(err, unit) {
					console.log('sendfrom '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err);
					cb(err, err ? undefined : unit);
				});
			}
			else
				headlessWallet.sendAssetFromAddress(asset, amount, from_address, to_address, null, function(err, unit) {
					console.log('sendfrom '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err);
					cb(err, err ? undefined : unit);
				});
		});
	});

	/**
	 * @typedef claimtextcoinResponse
	 * @property {string} unit
	 * @property {string} [asset]
	 */
	/**
	 * Claim the textcoin.<br>
	 * If address is invalid, then returns "invalid address".
	 * @name claimTextcoin
	 * @memberOf rpc_service
	 * @function
	 * @param {string} mnemonic - textcoin words
	 * @param {string} [address] - wallet address to receive funds
	 * @return {claimtextcoinResponse} unit ID and asset
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"claimtextcoin", "params":{mnemonic: "gym-cruise-upset-license-scan-viable-diary-release-corn-legal-bronze-mosquito"} }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('claimtextcoin', claimtextcoin);
	// aliases for claimtextcoin
	server.expose('sweeptextcoin', claimtextcoin);
	server.expose('sweeppaperwallet', claimtextcoin);

	function claimtextcoin(args, opt, cb) {
		console.log('claimtextcoin '+JSON.stringify(args));
		let start_time = Date.now();
		var {mnemonic, address} = args;
		if (Array.isArray(args)) {
			if (typeof args[0] === 'string')
				[mnemonic, address] = args;
			else
				return cb('mnemonic must be a string');
		}
		if (!mnemonic)
			return cb("mnemonic is required");
		if (address && !validationUtils.isValidAddress(address))
			return cb('invalid address');

		headlessWallet.readFirstAddress((first_address) => {
			address = address || first_address;
			Wallet.receiveTextCoin(mnemonic, address, function(err, unit, asset) {
				console.log('claimtextcoin '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err);
				cb(err, err ? undefined : {unit, asset});
			});
		});
	}

	/**
	 * Signs a message with address.<br>
	 * If address is invalid, then returns "invalid address".<br>
	 * If your wallet doesn`t own the address, then returns "address not found".
	 * @name signMessage
	 * @memberOf rpc_service
	 * @function
	 * @param {string} address - wallet that signs the message
	 * @param {string|object} message - message to be signed
	 * @return {string} base64 encoded signature
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"signmessage", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3", "Let there be light!"] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('signmessage', function(args, opt, cb) {
		var {address, message} = args;
		if (Array.isArray(args)) {
			if (typeof args[0] === 'string')
				[address, message] = args;
			else
				return cb('address must be a string');
		}
		if (address && !validationUtils.isValidAddress(address))
			return cb('invalid address');
		if (!message || (typeof message !== 'string' && typeof message !== 'object') || !Object.keys(message).length)
			return cb('message must be string or object');

		headlessWallet.readFirstAddress((first_address) => {
			address = address || first_address;
			db.query("SELECT definition FROM my_addresses WHERE address=?", [address], function(rows){
				if (rows.length !== 1)
					return cb("address not found");
				headlessWallet.signMessage(address, message, function(err, objSignedMessage){
					if (err)
						return cb(err);
					var signedMessageBase64 = Buffer.from(JSON.stringify(objSignedMessage)).toString('base64');
					cb(null, signedMessageBase64);
				});
			});
		});
	});

	/**
	 * @typedef {Object} verifymessageResponse
	 * @property {string} version
	 * @property {string|Object} signed_message
	 * @property {Object} authors
	 */
	/**
	 * Verifies signed message.
	 * @name verifyMessage
	 * @memberOf rpc_service
	 * @function
	 * @param {string} [address] - wallet that signed the message (first param can be null)
	 * @param {string} signature - base64 encoded signature
	 * @param {string|object} [message] - the message that was signed
	 * @return {verifymessageResponse} objSignedMessage
	 * @example
	 * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"verifymessage", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3", "TGV0IHRoZXJlIGJlIGxpZ2h0IQ==", "Let there be light!"] }' http://127.0.0.1:6332 | json_pp
	 */
	server.expose('verifymessage', verifymessage);
	// alias for verifymessage
	server.expose('validatemessage', verifymessage);

	function verifymessage(args, opt, cb) {
		var {address, signature, message} = args;
		if (Array.isArray(args)) {
			if (typeof args[1] === 'string')
				[address, signature, message] = args;
			else
				return cb('signature must be a string');
		}
		if (!validationUtils.isValidBase64(signature))
			return cb('signature is not valid base64');
		if (message && (typeof message !== 'string' && typeof message !== 'object' && !Object.keys(message).length))
			return cb('message must be string or object');

		var signedMessageJson = Buffer.from(signature, 'base64').toString('utf8');
		var objSignedMessage = {};
		try {
			objSignedMessage = JSON.parse(signedMessageJson);
		}
		catch(e) {
			return cb(e);
		}
		var signed_message = require('ocore/signed_message.js');
		signed_message.validateSignedMessage(db, objSignedMessage, address, function(err) {
			if (err)
				return cb(err);
			if (message) {
				if (typeof objSignedMessage.signed_message === "string" && objSignedMessage.signed_message !== message)
					return cb("message strings don't match");
				if (typeof objSignedMessage.signed_message === "object" && JSON.stringify(objSignedMessage.signed_message) !== JSON.stringify(message))
					return cb("message objects don't match");
			}
			cb(null, objSignedMessage);
		});
	}

	headlessWallet.readSingleWallet(function(_wallet_id) {
		wallet_id = _wallet_id;
		// listen creates an HTTP server on localhost only 
		var httpServer = server.listen(conf.rpcPort, conf.rpcInterface);
		httpServer.timeout = 900*1000;
	});
}

eventBus.on('headless_wallet_ready', initRPC);