From 80aa8f8c4cff7ee543440f8dc501315e327b3f48 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 5 Oct 2016 04:49:37 -0700 Subject: [PATCH] http: more methods and better validation. --- bin/cli | 79 ++++++++- lib/http/client.js | 123 ++++++++++++-- lib/http/server.js | 390 +++++++++++++++++++++++++++++++++++---------- lib/http/wallet.js | 84 ++++++++++ 4 files changed, 572 insertions(+), 104 deletions(-) diff --git a/bin/cli b/bin/cli index fd02a081..de80a0a7 100755 --- a/bin/cli +++ b/bin/cli @@ -7,7 +7,6 @@ var utils = require('../lib/utils/utils'); var co = require('../lib/utils/co'); var Client = require('../lib/http/client'); var Wallet = require('../lib/http/wallet'); -var assert = require('assert'); var main; function CLI() { @@ -38,8 +37,8 @@ CLI.prototype.createWallet = co(function* createWallet() { if (this.config.master) options.master = this.config.master; - if (this.config.key) - options.key = this.config.key; + if (this.config.mnemonic) + options.master = this.config.mnemonic; if (this.config.m) options.m = this.config.m >>> 0; @@ -65,13 +64,13 @@ CLI.prototype.createWallet = co(function* createWallet() { CLI.prototype.addKey = co(function* addKey() { var key = this.argv[0]; yield this.wallet.addKey(this.config.account, key); - this.log('added'); + this.log('Added key.'); }); CLI.prototype.removeKey = co(function* removeKey() { var key = this.argv[0]; yield this.wallet.removeKey(this.config.account, key); - this.log('removed'); + this.log('Removed key.'); }); CLI.prototype.getAccount = co(function* getAccount() { @@ -130,6 +129,8 @@ CLI.prototype.getTX = co(function* getTX() { CLI.prototype.getBlock = co(function* getBlock() { var hash = this.argv[0]; + var block; + if (hash.length !== 64) hash = +hash; @@ -137,7 +138,7 @@ CLI.prototype.getBlock = co(function* getBlock() { if (!block) { this.log('Block not found.'); - return + return; } this.log(block); @@ -268,7 +269,6 @@ CLI.prototype.zap = co(function* zap() { }); CLI.prototype.broadcast = co(function* broadcast() { - var self = this; var raw = this.argv[0] || this.config.tx; var tx = yield this.client.broadcast(raw); this.log('Broadcasted:'); @@ -292,6 +292,56 @@ CLI.prototype.retoken = co(function* retoken() { this.log(result); }); +CLI.prototype.rescan = co(function* rescan() { + var hash = this.argv[0]; + + if (hash.length !== 64) + hash = +hash; + + yield this.client.rescan(hash); + + this.log('Rescanning...'); +}); + +CLI.prototype.importKey = co(function* importKey() { + var key = this.argv[0]; + + if (!key) + throw new Error('No key for import.'); + + if (utils.isBase58(key)) { + yield this.wallet.importPrivate(key); + this.log('Imported private key.'); + return; + } + + if (utils.isHex(key)) { + yield this.wallet.importPublic(key); + this.log('Imported public key.'); + return; + } + + throw new Error('Bad key for import.'); +}); + +CLI.prototype.importAddress = co(function* importKey() { + var address = this.argv[0]; + yield this.wallet.importAddress(address); + this.log('Imported address.'); +}); + +CLI.prototype.lock = co(function* lock() { + yield this.wallet.lock(); + this.log('Locked.'); +}); + +CLI.prototype.unlock = co(function* unlock() { + var passphrase = this.argv[0]; + var timeout = +this.argv[1] || null; + yield this.wallet.unlock(passphrase, timeout); + this.log('Unlocked.'); +}); + CLI.prototype.rpc = co(function* rpc() { var method = this.argv.shift(); var params = []; @@ -369,6 +419,14 @@ CLI.prototype.handleWallet = co(function* handleWallet() { return yield this.getDetails(); case 'view': return yield this.viewTX(); + case 'import': + return yield this.importKey(); + case 'watch': + return yield this.importAddress(); + case 'lock': + return yield this.lock(); + case 'unlock': + return yield this.unlock(); default: this.log('Unrecognized command.'); this.log('Commands:'); @@ -390,6 +448,10 @@ CLI.prototype.handleWallet = co(function* handleWallet() { this.log(' $ zap --age [age]: Zap pending wallet TXs.'); this.log(' $ tx [hash]: View transaction details.'); this.log(' $ view [tx-hex]: Parse and view transaction.'); + this.log(' $ import [wif|hex]: Import private or public key.'); + this.log(' $ watch [address]: Import an address.'); + this.log(' $ lock: Lock wallet.'); + this.log(' $ unlock [passphrase] [timeout]: Unlock wallet.'); this.log('Other Options:'); this.log(' --passphrase [passphrase]: For signing and account creation.'); this.log(' --account [account-name]: Account name.'); @@ -421,6 +483,8 @@ CLI.prototype.handleNode = co(function* handleNode() { return yield this.getCoin(); case 'block': return yield this.getBlock(); + case 'rescan': + return yield this.rescan(); case 'rpc': return yield this.rpc(); default: @@ -432,6 +496,7 @@ CLI.prototype.handleNode = co(function* handleNode() { this.log(' $ tx [hash/address]: View transactions.'); this.log(' $ coin [hash+index/address]: View coins.'); this.log(' $ block [hash/height]: View block.'); + this.log(' $ rescan [height/hash]: Rescan for transactions.'); this.log(' $ rpc [command] [args]: Execute RPC command.'); return; } diff --git a/lib/http/client.js b/lib/http/client.js index 49568050..7f4fc84d 100644 --- a/lib/http/client.js +++ b/lib/http/client.js @@ -12,7 +12,6 @@ var AsyncObject = require('../utils/async'); var RPCClient = require('./rpcclient'); var utils = require('../utils/utils'); var co = require('../utils/co'); -var assert = require('assert'); var request = require('./request').promise; /** @@ -348,6 +347,17 @@ HTTPClient.prototype.broadcast = function broadcast(tx) { return this._post('/broadcast', body); }; +/** + * Rescan the chain. + * @param {Hash|Number} hash + * @returns {Promise} + */ + +HTTPClient.prototype.rescan = function rescan(hash) { + var options = { hash: hash }; + return this._post('/rescan', options); +}; + /** * Listen for events on wallet id. * @param {WalletID} id @@ -650,7 +660,6 @@ HTTPClient.prototype.zap = function zap(id, account, age) { account: account, age: age }; - assert(utils.isNumber(age)); return this._post('/wallet/' + id + '/zap', body); }; @@ -664,11 +673,7 @@ HTTPClient.prototype.zap = function zap(id, account, age) { */ HTTPClient.prototype.addKey = function addKey(id, account, key) { - var options; - - key = key.xpubkey || key; - options = { account: account, key: key }; - + var options = { account: account, accountKey: key }; return this._put('/wallet/' + id + '/key', options); }; @@ -682,14 +687,108 @@ HTTPClient.prototype.addKey = function addKey(id, account, key) { */ HTTPClient.prototype.removeKey = function removeKey(id, account, key) { - var options; - - key = key.xpubkey || key; - options = { account: account, key: key }; - + var options = { account: account, accountKey: key }; return this._del('/wallet/' + id + '/key', options); }; +/** + * Import private key. + * @param {String} id + * @param {Number|String} account + * @param {String} key + * @returns {Promise} + */ + +HTTPClient.prototype.importPrivate = function importPrivate(id, account, key) { + var options = { privateKey: key }; + return this._post('/wallet/' + id + '/import', options); +}; + +/** + * Import public key. + * @param {String} id + * @param {Number|String} account + * @param {String} key + * @returns {Promise} + */ + +HTTPClient.prototype.importPublic = function importPublic(id, account, key) { + var options = { publicKey: key }; + return this._post('/wallet/' + id + '/import', options); +}; + +/** + * Import address. + * @param {String} id + * @param {Number|String} account + * @param {String} address + * @returns {Promise} + */ + +HTTPClient.prototype.importAddress = function importAddress(id, account, address) { + var options = { address: address }; + return this._post('/wallet/' + id + '/import', options); +}; + +/** + * Lock a coin. + * @param {String} id + * @param {String} hash + * @param {Number} index + * @returns {Promise} + */ + +HTTPClient.prototype.lockCoin = function lockCoin(id, hash, index) { + var options = { hash: hash, index: index }; + return this._put('/wallet/' + id + '/coin/locked', options); +}; + +/** + * Unlock a coin. + * @param {String} id + * @param {String} hash + * @param {Number} index + * @returns {Promise} + */ + +HTTPClient.prototype.unlockCoin = function unlockCoin(id, hash, index) { + var options = { hash: hash, index: index }; + return this._del('/wallet/' + id + '/coin/locked', options); +}; + +/** + * Get locked coins. + * @param {String} id + * @returns {Promise} + */ + +HTTPClient.prototype.getLocked = function getLocked(id) { + return this._get('/wallet/' + id + '/coin/locked'); +}; + +/** + * Lock wallet. + * @param {String} id + * @returns {Promise} + */ + +HTTPClient.prototype.lock = function lock(id) { + return this._post('/wallet/' + id + '/lock', {}); +}; + +/** + * Unlock wallet. + * @param {String} id + * @param {String} passphrase + * @param {Number} timeout + * @returns {Promise} + */ + +HTTPClient.prototype.unlock = function unlock(id, passphrase, timeout) { + var options = { passphrase: passphrase, timeout: timeout }; + return this._post('/wallet/' + id + '/unlock', options); +}; + /** * Get wallet accounts. * @param {WalletID} id diff --git a/lib/http/server.js b/lib/http/server.js index c64fc0e0..0508d3fc 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -8,17 +8,21 @@ 'use strict'; /* jshint -W069 */ +/* jshint noyield: true */ var EventEmitter = require('events').EventEmitter; +var assert = require('assert'); var constants = require('../protocol/constants'); var HTTPBase = require('./base'); var utils = require('../utils/utils'); var co = require('../utils/co'); var Address = require('../primitives/address'); var TX = require('../primitives/tx'); +var KeyRing = require('../primitives/keyring'); +var Outpoint = require('../primitives/outpoint'); +var HD = require('../hd/hd'); var Script = require('../script/script'); var crypto = require('../crypto/crypto'); -var assert = require('assert'); var con = co.con; var RPC; @@ -176,8 +180,8 @@ HTTPServer.prototype._init = function _init() { var i, params, options, output, address; if (req.method === 'POST' && req.pathname === '/') { - assert(typeof req.body.method === 'string', 'Method must be a string.'); - assert(Array.isArray(req.body.params), 'Params must be an array.'); + enforce(typeof req.body.method === 'string', 'Method must be a string.'); + enforce(Array.isArray(req.body.params), 'Params must be an array.'); req.options = {}; return next(); } @@ -193,15 +197,15 @@ HTTPServer.prototype._init = function _init() { this.logger.debug(params); if (params.id) { - assert(typeof params.id === 'string', 'ID must be a string.'); + enforce(typeof params.id === 'string', 'ID must be a string.'); options.id = params.id; } if (params.hash) { - assert(typeof params.hash === 'string', 'Hash must be a string.'); + enforce(typeof params.hash === 'string', 'Hash must be a string.'); if (params.hash.length !== 64) { options.height = Number(params.hash); - assert(utils.isUInt32(options.height), 'Height must be a number.'); + enforce(utils.isUInt32(options.height), 'Height must be a number.'); } else { options.hash = utils.revHex(params.hash); } @@ -209,37 +213,37 @@ HTTPServer.prototype._init = function _init() { if (params.index != null) { options.index = Number(params.index); - assert(utils.isUInt32(options.index), 'Index must be a number.'); + enforce(utils.isUInt32(options.index), 'Index must be a number.'); } if (params.height != null) { options.height = Number(params.height); - assert(utils.isUInt32(options.height), 'Height must be a number.'); + enforce(utils.isUInt32(options.height), 'Height must be a number.'); } if (params.start != null) { options.start = Number(params.start); - assert(utils.isUInt32(options.start), 'Start must be a number.'); + enforce(utils.isUInt32(options.start), 'Start must be a number.'); } if (params.end != null) { options.end = Number(params.end); - assert(utils.isUInt32(options.end), 'End must be a number.'); + enforce(utils.isUInt32(options.end), 'End must be a number.'); } if (params.limit != null) { options.limit = Number(params.limit); - assert(utils.isUInt32(options.limit), 'Limit must be a number.'); + enforce(utils.isUInt32(options.limit), 'Limit must be a number.'); } if (params.age != null) { options.age = Number(params.age); - assert(utils.isUInt32(options.age), 'Age must be a number.'); + enforce(utils.isUInt32(options.age), 'Age must be a number.'); } if (params.confirmations != null) { options.confirmations = Number(params.confirmations); - assert(utils.isNumber(options.confirmations), + enforce(utils.isNumber(options.confirmations), 'Confirmations must be a number.'); } @@ -257,53 +261,63 @@ HTTPServer.prototype._init = function _init() { if (params.m != null) { options.m = Number(params.m); - assert(utils.isUInt32(options.m), 'm must be a number.'); + enforce(utils.isUInt32(options.m), 'm must be a number.'); } if (params.n != null) { options.n = Number(params.n); - assert(utils.isUInt32(options.n), 'n must be a number.'); + enforce(utils.isUInt32(options.n), 'n must be a number.'); } if (params.blocks != null) { options.blocks = Number(params.blocks); - assert(utils.isUInt32(options.blocks), 'Blocks must be a number.'); + enforce(utils.isUInt32(options.blocks), 'Blocks must be a number.'); } if (params.subtractFee != null) { if (typeof params.subtractFee === 'number') { options.subtractFee = params.subtractFee; - assert(utils.isUInt32(options.subtractFee), 'subtractFee must be a number.'); + enforce(utils.isUInt32(options.subtractFee), 'subtractFee must be a number.'); } else { options.subtractFee = params.subtractFee; - assert(typeof options.subtractFee === 'boolean', 'subtractFee must be a boolean.'); + enforce(typeof options.subtractFee === 'boolean', 'subtractFee must be a boolean.'); } } if (params.watchOnly != null) { - assert(typeof params.watchOnly === 'boolean', 'watchOnly must be a boolean.'); + enforce(typeof params.watchOnly === 'boolean', 'watchOnly must be a boolean.'); options.watchOnly = params.watchOnly; } if (params.accountKey) { - assert(typeof params.accountKey === 'string', 'accountKey must be a string.'); - options.accountKey = params.accountKey; + enforce(typeof params.accountKey === 'string', 'accountKey must be a string.'); + options.accountKey = HD.fromExtended(params.accountKey); + } + + if (params.timeout != null) { + options.timeout = Number(params.timeout); + enforce(utils.isNumber(options.timeout), 'Timeout must be a number.'); + } + + if (params.witness != null) { + enforce(typeof params.witness === 'boolean', 'witness must be a boolean.'); + options.witness = params.witness; } if (params.outputs) { - assert(Array.isArray(params.outputs), 'Outputs must be an array.'); + enforce(Array.isArray(params.outputs), 'Outputs must be an array.'); options.outputs = []; for (i = 0; i < params.outputs.length; i++) { output = params.outputs[i]; - assert(output && typeof output === 'object', 'Output must be an object.'); + enforce(output && typeof output === 'object', 'Output must be an object.'); if (output.address) - assert(typeof output.address === 'string', 'Address must be a string.'); + enforce(typeof output.address === 'string', 'Address must be a string.'); else if (output.script) - assert(typeof output.script === 'string', 'Script must be a string.'); + enforce(typeof output.script === 'string', 'Script must be a string.'); else - assert(false, 'No address or script present.'); + enforce(false, 'No address or script present.'); options.outputs.push({ address: output.address @@ -322,11 +336,11 @@ HTTPServer.prototype._init = function _init() { options.address = []; for (i = 0; i < params.address.length; i++) { address = params.address[i]; - assert(typeof address === 'string', 'Address must be a string.'); + enforce(typeof address === 'string', 'Address must be a string.'); address = Address.fromBase58(address); } } else { - assert(typeof params.address === 'string', 'Address must be a string.'); + enforce(typeof params.address === 'string', 'Address must be a string.'); options.address = Address.fromBase58(params.address); } } @@ -335,7 +349,7 @@ HTTPServer.prototype._init = function _init() { if (typeof params.tx === 'object') { options.tx = TX.fromJSON(params.tx); } else { - assert(typeof params.tx === 'string', 'TX must be a hex string.'); + enforce(typeof params.tx === 'string', 'TX must be a hex string.'); options.tx = TX.fromRaw(params.tx, 'hex'); } } @@ -343,43 +357,59 @@ HTTPServer.prototype._init = function _init() { if (params.account != null) { if (typeof params.account === 'number') { options.account = params.account; - assert(utils.isUInt32(options.account), 'Account must be a number.'); + enforce(utils.isUInt32(options.account), 'Account must be a number.'); } else { - assert(typeof params.account === 'string', 'Account must be a string.'); + enforce(typeof params.account === 'string', 'Account must be a string.'); options.account = params.account; } } if (params.type) { - assert(typeof params.type === 'string', 'Type must be a string.'); + enforce(typeof params.type === 'string', 'Type must be a string.'); options.type = params.type; } if (params.name) { - assert(typeof params.name === 'string', 'Name must be a string.'); + enforce(typeof params.name === 'string', 'Name must be a string.'); options.name = params.name; } - if (params.key) { - assert(typeof params.key === 'string', 'Key must be a string.'); - options.key = params.key; + if (params.privateKey) { + enforce(typeof params.privateKey === 'string', 'Key must be a string.'); + options.privateKey = KeyRing.fromSecret(params.privateKey); + } + + if (params.publicKey) { + enforce(typeof params.publicKey === 'string', 'Key must be a string.'); + options.publicKey = new Buffer(params.publicKey, 'hex'); + options.publicKey = KeyRing.fromKey(options.publicKey, this.network); + } + + if (params.master) { + enforce(typeof params.key === 'string', 'Key must be a string.'); + options.master = HD.fromExtended(params.master); + } + + if (params.mnemonic) { + enforce(typeof params.mnemonic === 'string', 'Key must be a string.'); + options.master = HD.fromMnemonic(params.mnemonic, this.network); } if (params.old) { - assert(typeof params.old === 'string', 'Passphrase must be a string.'); - assert(params.old.length > 0, 'Passphrase must be a string.'); + enforce(typeof params.old === 'string', 'Passphrase must be a string.'); + enforce(params.old.length > 0, 'Passphrase must be a string.'); options.old = params.old; } if (params.passphrase) { - assert(typeof params.passphrase === 'string', 'Passphrase must be a string.'); - assert(params.passphrase.length > 0, 'Passphrase must be a string.'); + enforce(typeof params.passphrase === 'string', 'Passphrase must be a string.'); + enforce(params.passphrase.length > 0, 'Passphrase must be a string.'); options.passphrase = params.passphrase; } if (params.token) { - assert(utils.isHex(params.token), 'Wallet token must be a hex string.'); - assert(params.token.length === 64, 'Wallet token must be 32 bytes.'); + enforce(utils.isHex(params.token), 'Wallet token must be a hex string.'); + enforce(params.token.length === 64, 'Wallet token must be 32 bytes.'); options.token = new Buffer(params.token, 'hex'); } @@ -425,7 +455,9 @@ HTTPServer.prototype._init = function _init() { } req.wallet = wallet; + this.logger.info('Successful auth for %s.', req.options.id); + next(); })); @@ -489,7 +521,12 @@ HTTPServer.prototype._init = function _init() { // UTXO by address this.get('/coin/address/:address', con(function* (req, res, send, next) { - var coins = yield this.node.getCoinsByAddress(req.options.address); + var coins; + + enforce(req.options.address, 'Address is required.'); + + coins = yield this.node.getCoinsByAddress(req.options.address); + send(200, coins.map(function(coin) { return coin.toJSON(); })); @@ -497,7 +534,12 @@ HTTPServer.prototype._init = function _init() { // UTXO by id this.get('/coin/:hash/:index', con(function* (req, res, send, next) { - var coin = yield this.node.getCoin(req.options.hash, req.options.index); + var coin; + + enforce(req.options.hash, 'Hash is required.'); + enforce(req.options.index != null, 'Index is required.'); + + coin = yield this.node.getCoin(req.options.hash, req.options.index); if (!coin) return send(404); @@ -507,7 +549,12 @@ HTTPServer.prototype._init = function _init() { // Bulk read UTXOs this.post('/coin/address', con(function* (req, res, send, next) { - var coins = yield this.node.getCoinsByAddress(req.options.address); + var coins; + + enforce(req.options.address, 'Address is required.'); + + coins = yield this.node.getCoinsByAddress(req.options.address); + send(200, coins.map(function(coin) { return coin.toJSON(); })); @@ -515,7 +562,11 @@ HTTPServer.prototype._init = function _init() { // TX by hash this.get('/tx/:hash', con(function* (req, res, send, next) { - var tx = yield this.node.getTX(req.options.hash); + var tx; + + enforce(req.options.hash, 'Hash is required.'); + + tx = yield this.node.getTX(req.options.hash); if (!tx) return send(404); @@ -527,8 +578,11 @@ HTTPServer.prototype._init = function _init() { // TX by address this.get('/tx/address/:address', con(function* (req, res, send, next) { - var txs = yield this.node.getTXByAddress(req.options.address); - var i, tx; + var i, txs, tx; + + enforce(req.options.address, 'Address is required.'); + + txs = yield this.node.getTXByAddress(req.options.address); for (i = 0; i < txs.length; i++) { tx = txs[i]; @@ -542,8 +596,11 @@ HTTPServer.prototype._init = function _init() { // Bulk read TXs this.post('/tx/address', con(function* (req, res, send, next) { - var txs = yield this.node.getTXByAddress(req.options.address); - var i, tx; + var i, txs, tx; + + enforce(req.options.address, 'Address is required.'); + + txs = yield this.node.getTXByAddress(req.options.address); for (i = 0; i < txs.length; i++) { tx = txs[i]; @@ -558,7 +615,11 @@ HTTPServer.prototype._init = function _init() { // Block by hash/height this.get('/block/:hash', con(function* (req, res, send, next) { var hash = req.options.hash || req.options.height; - var block = yield this.node.getFullBlock(hash); + var block; + + enforce(hash != null, 'Hash or height required.'); + + block = yield this.node.getFullBlock(hash); if (!block) return send(404); @@ -587,6 +648,7 @@ HTTPServer.prototype._init = function _init() { // Broadcast TX this.post('/broadcast', con(function* (req, res, send, next) { + enforce(req.options.tx, 'TX is required.'); yield this.node.sendTX(req.options.tx); send(200, { success: true }); })); @@ -603,6 +665,15 @@ HTTPServer.prototype._init = function _init() { send(200, { rate: utils.btc(fee) }); }); + // Rescan + this.post('/rescan', con(function* (req, res, send, next) { + var options = req.options; + var height = options.hash || options.height; + enforce(height != null, 'Hash or height is required.'); + send(200, { success: true }); + yield this.node.scan(height); + })); + // Get wallet this.get('/wallet/:id', function(req, res, send, next) { send(200, req.wallet.toJSON()); @@ -622,7 +693,13 @@ HTTPServer.prototype._init = function _init() { // Get account this.get('/wallet/:id/account/:account', con(function* (req, res, send, next) { - var account = yield req.wallet.getAccount(req.options.account); + var options = req.options; + var acct = options.name || options.account; + var account; + + enforce(acct != null, 'Account is required.'); + + account = yield req.wallet.getAccount(acct); if (!account) return send(404); @@ -630,9 +707,17 @@ HTTPServer.prototype._init = function _init() { send(200, account.toJSON()); })); - // Create/get account + // Create account this.post('/wallet/:id/account/:account?', con(function* (req, res, send, next) { - var account = yield req.wallet.createAccount(req.options); + var options = req.options; + var account; + + if (typeof options.account === 'string') { + options.name = options.account; + options.account = null; + } + + account = yield req.wallet.createAccount(req.options); if (!account) return send(404); @@ -645,10 +730,49 @@ HTTPServer.prototype._init = function _init() { var options = req.options; var old = options.old; var new_ = options.passphrase; + enforce(old || new_, 'Passphrase is required.'); yield req.wallet.setPassphrase(old, new_); send(200, { success: true }); })); + // Unlock wallet + this.post('/wallet/:id/unlock', con(function* (req, res, send, next) { + var options = req.options; + var passphrase = options.passphrase; + var timeout = options.timeout; + enforce(passphrase, 'Passphrase is required.'); + yield req.wallet.unlock(passphrase, timeout); + send(200, { success: true }); + })); + + // Lock wallet + this.post('/wallet/:id/lock', con(function* (req, res, send, next) { + yield req.wallet.lock(); + send(200, { success: true }); + })); + + // Import key + this.post('/wallet/:id/import', con(function* (req, res, send, next) { + var options = req.options; + var acct = req.options.name || req.options.account; + var key = options.privateKey || options.publicKey; + + if (key) { + yield req.wallet.importKey(acct, key); + send(200, { success: true }); + return; + } + + if (options.address) { + enforce(options.address instanceof Address, 'Address is required.'); + yield req.wallet.importAddress(acct, options.address); + send(200, { success: true }); + return; + } + + enforce(false, 'Key or address is required.'); + })); + // Generate new token this.post('/wallet/:id/retoken', con(function* (req, res, send, next) { var options = req.options; @@ -675,6 +799,7 @@ HTTPServer.prototype._init = function _init() { this.post('/wallet/:id/sign', con(function* (req, res, send, next) { var options = req.options; var tx = req.options.tx; + enforce(tx, 'TX is required.'); yield req.wallet.sign(tx, options); send(200, tx.toJSON()); })); @@ -682,59 +807,70 @@ HTTPServer.prototype._init = function _init() { // Fill TX this.post('/wallet/:id/fill', con(function* (req, res, send, next) { var tx = req.options.tx; + enforce(tx, 'TX is required.'); yield req.wallet.fillHistory(tx); send(200, tx.toJSON()); })); // Zap Wallet TXs this.post('/wallet/:id/zap', con(function* (req, res, send, next) { - var account = req.options.account; - var age = req.options.age; - yield req.wallet.zap(account, age); + var options = req.options; + var acct = options.name || options.account; + var age = options.age; + enforce(age, 'Age is required.'); + yield req.wallet.zap(acct, age); send(200, { success: true }); })); // Abandon Wallet TX this.del('/wallet/:id/tx/:hash', con(function* (req, res, send, next) { var hash = req.options.hash; + enforce(hash, 'Hash is required.'); yield req.wallet.abandon(hash); send(200, { success: true }); })); // Add key this.put('/wallet/:id/key', con(function* (req, res, send, next) { - var account = req.options.account; - var key = req.options.key; - yield req.wallet.addKey(account, key); + var options = req.options; + var acct = options.name || options.account; + var key = options.accountKey; + enforce(key, 'Key is required.'); + yield req.wallet.addKey(acct, key); send(200, { success: true }); })); // Remove key this.del('/wallet/:id/key', con(function* (req, res, send, next) { - var account = req.options.account; - var key = req.options.key; - yield req.wallet.removeKey(account, key); + var options = req.options; + var acct = options.name || options.account; + var key = options.accountKey; + enforce(key, 'Key is required.'); + yield req.wallet.removeKey(acct, key); send(200, { success: true }); })); // Create address this.post('/wallet/:id/address', con(function* (req, res, send, next) { - var account = req.options.account; - var address = yield req.wallet.createReceive(account); + var options = req.options; + var acct = options.name || options.account; + var address = yield req.wallet.createReceive(acct); send(200, address.toJSON()); })); // Create nested address this.post('/wallet/:id/nested', con(function* (req, res, send, next) { - var account = req.options.account; - var address = yield req.wallet.createNested(account); + var options = req.options; + var acct = options.name || options.account; + var address = yield req.wallet.createNested(acct); send(200, address.toJSON()); })); // Wallet Balance this.get('/wallet/:id/balance', con(function* (req, res, send, next) { - var account = req.options.account; - var balance = yield req.wallet.getBalance(account); + var options = req.options; + var acct = options.name || options.account; + var balance = yield req.wallet.getBalance(acct); if (!balance) return send(404); @@ -744,18 +880,61 @@ HTTPServer.prototype._init = function _init() { // Wallet UTXOs this.get('/wallet/:id/coin', con(function* (req, res, send, next) { - var account = req.options.account; - var coins = yield req.wallet.getCoins(account); + var options = req.options; + var acct = options.name || options.account; + var coins = yield req.wallet.getCoins(acct); + + sortCoins(coins); + send(200, coins.map(function(coin) { return coin.toJSON(); })); })); + // Locked coins + this.get('/wallet/:id/coin/locked', con(function* (req, res, send, next) { + var locked = this.wallet.getLocked(); + send(200, locked.map(function(outpoint) { + return outpoint.toJSON(); + })); + })); + + // Lock coin + this.put('/wallet/:id/coin/locked', con(function* (req, res, send, next) { + var options = req.options.hash; + var outpoint; + + enforce(options.hash, 'Hash is required.'); + enforce(options.index != null, 'Index is required.'); + + outpoint = new Outpoint(options.hash, options.index); + + this.wallet.lockCoin(outpoint); + })); + + // Unlock coin + this.del('/wallet/:id/coin/locked', con(function* (req, res, send, next) { + var options = req.options.hash; + var outpoint; + + enforce(options.hash, 'Hash is required.'); + enforce(options.index != null, 'Index is required.'); + + outpoint = new Outpoint(options.hash, options.index); + + this.wallet.unlockCoin(outpoint); + })); + // Wallet Coin this.get('/wallet/:id/coin/:hash/:index', con(function* (req, res, send, next) { var hash = req.options.hash; var index = req.options.index; - var coin = yield req.wallet.getCoin(hash, index); + var coin; + + enforce(hash, 'Hash is required.'); + enforce(index != null, 'Index is required.'); + + coin = yield req.wallet.getCoin(hash, index); if (!coin) return send(404); @@ -765,9 +944,15 @@ HTTPServer.prototype._init = function _init() { // Wallet TXs this.get('/wallet/:id/tx/history', con(function* (req, res, send, next) { - var account = req.options.account; - var txs = yield req.wallet.getHistory(account); - var details = yield req.wallet.toDetails(txs); + var options = req.options; + var acct = options.name || options.account; + var txs = yield req.wallet.getHistory(acct); + var details; + + sortTX(txs); + + details = yield req.wallet.toDetails(txs); + send(200, details.map(function(tx) { return tx.toJSON(); })); @@ -775,9 +960,15 @@ HTTPServer.prototype._init = function _init() { // Wallet Pending TXs this.get('/wallet/:id/tx/unconfirmed', con(function* (req, res, send, next) { - var account = req.options.account; - var txs = yield req.wallet.getUnconfirmed(account); - var details = yield req.wallet.toDetails(txs); + var options = req.options; + var acct = options.name || options.account; + var txs = yield req.wallet.getUnconfirmed(acct); + var details; + + sortTX(txs); + + details = yield req.wallet.toDetails(txs); + send(200, details.map(function(tx) { return tx.toJSON(); })); @@ -785,9 +976,9 @@ HTTPServer.prototype._init = function _init() { // Wallet TXs within time range this.get('/wallet/:id/tx/range', con(function* (req, res, send, next) { - var account = req.options.account; var options = req.options; - var txs = yield req.wallet.getRange(account, options); + var acct = options.name || options.account; + var txs = yield req.wallet.getRange(acct, options); var details = yield req.wallet.toDetails(txs); send(200, details.map(function(tx) { return tx.toJSON(); @@ -796,9 +987,10 @@ HTTPServer.prototype._init = function _init() { // Last Wallet TXs this.get('/wallet/:id/tx/last', con(function* (req, res, send, next) { - var account = req.options.account; - var limit = req.options.limit; - var txs = yield req.wallet.getLast(account, limit); + var options = req.options; + var acct = options.name || options.account; + var limit = options.limit; + var txs = yield req.wallet.getLast(acct, limit); var details = yield req.wallet.toDetails(txs); send(200, details.map(function(tx) { return tx.toJSON(); @@ -808,13 +1000,17 @@ HTTPServer.prototype._init = function _init() { // Wallet TX this.get('/wallet/:id/tx/:hash', con(function* (req, res, send, next) { var hash = req.options.hash; - var tx = yield req.wallet.getTX(hash); - var details; + var tx, details; + + enforce(hash, 'Hash is required.'); + + tx = yield req.wallet.getTX(hash); if (!tx) return send(404); details = yield req.wallet.toDetails(tx); + send(200, details.toJSON()); })); @@ -1373,6 +1569,30 @@ function softMerge(a, b, soft) { } } +function enforce(value, msg) { + var err; + + if (!value) { + err = new Error(msg); + err.statusCode = 400; + throw err; + } +} + +function sortTX(txs) { + return txs.sort(function(a, b) { + return a.ps - b.ps; + }); +} + +function sortCoins(coins) { + return coins.sort(function(a, b) { + a = a.height === -1 ? 0x7fffffff : a.height; + b = b.height === -1 ? 0x7fffffff : b.height; + return a - b; + }); +} + /* * Expose */ diff --git a/lib/http/wallet.js b/lib/http/wallet.js index 8aad9cfa..bd754409 100644 --- a/lib/http/wallet.js +++ b/lib/http/wallet.js @@ -309,6 +309,90 @@ HTTPWallet.prototype.retoken = co(function* retoken(passphrase) { return token; }); +/** + * Import private key. + * @param {Number|String} account + * @param {String} key + * @returns {Promise} + */ + +HTTPWallet.prototype.importPrivate = function importPrivate(id, account, key) { + return this.client.importPrivate(this.id, account, key); +}; + +/** + * Import public key. + * @param {Number|String} account + * @param {String} key + * @returns {Promise} + */ + +HTTPWallet.prototype.importPublic = function importPublic(id, account, key) { + return this.client.importPublic(this.id, account, key); +}; + +/** + * Import address. + * @param {Number|String} account + * @param {String} address + * @returns {Promise} + */ + +HTTPWallet.prototype.importAddress = function importAddress(id, account, address) { + return this.client.importAddress(this.id, account, address); +}; + +/** + * Lock a coin. + * @param {String} hash + * @param {Number} index + * @returns {Promise} + */ + +HTTPWallet.prototype.lockCoin = function lockCoin(id, hash, index) { + return this.client.lockCoin(this.id, hash, index); +}; + +/** + * Unlock a coin. + * @param {String} hash + * @param {Number} index + * @returns {Promise} + */ + +HTTPWallet.prototype.unlockCoin = function unlockCoin(id, hash, index) { + return this.client.unlockCoin(this.id, hash, index); +}; + +/** + * Get locked coins. + * @returns {Promise} + */ + +HTTPWallet.prototype.getLocked = function getLocked(id) { + return this.client.getLocked(this.id); +}; + +/** + * Lock wallet. + * @returns {Promise} + */ + +HTTPWallet.prototype.lock = function lock(id) { + return this.client.lock(this.id); +}; + +/** + * Unlock wallet. + * @param {String} passphrase + * @param {Number} timeout + * @returns {Promise} + */ + +HTTPWallet.prototype.unlock = function unlock(id, passphrase, timeout) { + return this.client.unlock(this.id, passphrase, timeout); +}; + /* * Expose */