diff --git a/app.js b/app.js index 9519be1..7c73f44 100644 --- a/app.js +++ b/app.js @@ -1,252 +1,10 @@ -'use strict'; - -var _ = require('lodash'); -var async = require('async'); -var log = require('npmlog'); -var express = require('express'); -var querystring = require('querystring'); -var bodyParser = require('body-parser') - -var CopayServer = require('./lib/server'); - -log.debug = log.verbose; -log.level = 'debug'; - - -CopayServer.initialize(); - - -var app = express(); -app.use(function(req, res, next) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE'); - res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,Authorization'); - next(); -}); -var allowCORS = function(req, res, next) { - if ('OPTIONS' == req.method) { - res.send(200); - res.end(); - return; - } - next(); -} -app.use(allowCORS); - -var POST_LIMIT = 1024 * 100 /* Max POST 100 kb */ ; - -app.use(bodyParser.json({ - limit: POST_LIMIT -})); - -app.use(require('morgan')('dev')); +#!/usr/bin/env node +var ExpressApp = require('./lib/expressapp'); var port = process.env.COPAY_PORT || 3001; -var router = express.Router(); - -function returnError(err, res, req) { - if (err instanceof CopayServer.ClientError) { - - var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; - log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message); - res.status(status).json({ - code: err.code, - error: err.message, - }).end(); - } else { - var code, message; - if (_.isObject(err)) { - code = err.code; - message = err.message; - } - var m = message || err.toString(); - - log.error('Error: ' + req.url + ' :' + code + ':' + m); - res.status(code || 500).json({ - error: m, - }).end(); - } -}; - -function getCredentials(req) { - var identity = req.header('x-identity'); - if (!identity) return; - - return { - copayerId: identity, - signature: req.header('x-signature'), - }; -}; - -function getServerWithAuth(req, res, cb) { - var credentials = getCredentials(req); - var auth = { - copayerId: credentials.copayerId, - message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body), - signature: credentials.signature, - }; - CopayServer.getInstanceWithAuth(auth, function(err, server) { - if (err) return returnError(err, res, req); - return cb(server); - }); -}; - -router.post('/v1/wallets/', function(req, res) { - var server = CopayServer.getInstance(); - server.createWallet(req.body, function(err, walletId) { - if (err) return returnError(err, res, req); - - res.json({ - walletId: walletId, - }); - }); -}); - -router.post('/v1/wallets/:id/copayers/', function(req, res) { - req.body.walletId = req.params['id']; - var server = CopayServer.getInstance(); - server.joinWallet(req.body, function(err, result) { - if (err) return returnError(err, res, req); - - res.json(result); - }); -}); - -router.get('/v1/wallets/', function(req, res) { - getServerWithAuth(req, res, function(server) { - var result = {}; - async.parallel([ - - function(next) { - server.getWallet({}, function(err, wallet) { - if (err) return next(err); - result.wallet = wallet; - next(); - }); - }, - function(next) { - server.getBalance({}, function(err, balance) { - if (err) return next(err); - result.balance = balance; - next(); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, pendingTxps) { - if (err) return next(err); - result.pendingTxps = pendingTxps; - next(); - }); - }, - ], function(err) { - if (err) return returnError(err, res, req); - res.json(result); - }); - }); -}); - - -router.get('/v1/txproposals/', function(req, res) { - getServerWithAuth(req, res, function(server) { - server.getPendingTxs({}, function(err, pendings) { - if (err) return returnError(err, res, req); - res.json(pendings); - }); - }); -}); - - -router.post('/v1/txproposals/', function(req, res) { - getServerWithAuth(req, res, function(server) { - server.createTx(req.body, function(err, txp) { - if (err) return returnError(err, res, req); - res.json(txp); - }); - }); -}); - - -router.post('/v1/addresses/', function(req, res) { - getServerWithAuth(req, res, function(server) { - server.createAddress(req.body, function(err, address) { - if (err) return returnError(err, res, req); - res.json(address); - }); - }); -}); - -router.get('/v1/addresses/', function(req, res) { - getServerWithAuth(req, res, function(server) { - server.getAddresses({}, function(err, addresses) { - if (err) return returnError(err, res, req); - res.json(addresses); - }); - }); -}); - -router.get('/v1/balance/', function(req, res) { - getServerWithAuth(req, res, function(server) { - server.getBalance({}, function(err, balance) { - if (err) return returnError(err, res, req); - res.json(balance); - }); - }); -}); - -router.post('/v1/txproposals/:id/signatures/', function(req, res) { - getServerWithAuth(req, res, function(server) { - req.body.txProposalId = req.params['id']; - server.signTx(req.body, function(err, txp) { - if (err) return returnError(err, res, req); - res.json(txp); - res.end(); - }); - }); -}); - -// TODO Check HTTP verb and URL name -router.post('/v1/txproposals/:id/broadcast/', function(req, res) { - getServerWithAuth(req, res, function(server) { - req.body.txProposalId = req.params['id']; - server.broadcastTx(req.body, function(err, txp) { - if (err) return returnError(err, res, req); - res.json(txp); - res.end(); - }); - }); -}); - -router.post('/v1/txproposals/:id/rejections', function(req, res) { - getServerWithAuth(req, res, function(server) { - req.body.txProposalId = req.params['id']; - server.rejectTx(req.body, function(err, txp) { - if (err) return returnError(err, res, req); - res.json(txp); - res.end(); - }); - }); -}); - -router.delete('/v1/txproposals/:id/', function(req, res) { - getServerWithAuth(req, res, function(server) { - req.body.txProposalId = req.params['id']; - server.removePendingTx(req.body, function(err) { - if (err) return returnError(err, res, req); - res.end(); - }); - }); -}); - -// TODO: DEBUG only! -router.get('/v1/dump', function(req, res) { - var server = CopayServer.getInstance(); - server.storage._dump(function() { - res.end(); - }); -}); - -app.use('/copay/api', router); +var app = ExpressApp.start(); app.listen(port); + console.log('Copay service running on port ' + port); diff --git a/lib/client/api.js b/lib/client/api.js index f2224ff..9020dbb 100644 --- a/lib/client/api.js +++ b/lib/client/api.js @@ -127,6 +127,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) { var absUrl = this.baseUrl + url; var args = { + relUrl: url, headers: { 'x-identity': data.copayerId, 'x-signature': reqSignature, diff --git a/lib/expressapp.js b/lib/expressapp.js new file mode 100644 index 0000000..b39d61e --- /dev/null +++ b/lib/expressapp.js @@ -0,0 +1,254 @@ +'use strict'; + +var _ = require('lodash'); +var async = require('async'); +var log = require('npmlog'); +var express = require('express'); +var querystring = require('querystring'); +var bodyParser = require('body-parser') + +var CopayServer = require('./server'); + +log.debug = log.verbose; +log.level = 'debug'; + +var ExpressApp = function() {}; + +ExpressApp.start = function(opts) { + opts = opts || {}; + + + CopayServer.initialize(opts.CopayServer); + var app = express(); + app.use(function(req, res, next) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE'); + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type,Authorization'); + next(); + }); + var allowCORS = function(req, res, next) { + if ('OPTIONS' == req.method) { + res.send(200); + res.end(); + return; + } + next(); + } + app.use(allowCORS); + + var POST_LIMIT = 1024 * 100 /* Max POST 100 kb */ ; + + app.use(bodyParser.json({ + limit: POST_LIMIT + })); + + app.use(require('morgan')('dev')); + + + var router = express.Router(); + + function returnError(err, res, req) { + if (err instanceof CopayServer.ClientError) { + + var status = (err.code == 'NOTAUTHORIZED') ? 401 : 400; + log.error('Err: ' + status + ':' + req.url + ' :' + err.code + ':' + err.message); + res.status(status).json({ + code: err.code, + error: err.message, + }).end(); + } else { + var code, message; + if (_.isObject(err)) { + code = err.code; + message = err.message; + } + var m = message || err.toString(); + + log.error('Error: ' + req.url + ' :' + code + ':' + m); + res.status(code || 500).json({ + error: m, + }).end(); + } + }; + + function getCredentials(req) { + var identity = req.header('x-identity'); + if (!identity) return; + + return { + copayerId: identity, + signature: req.header('x-signature'), + }; + }; + + function getServerWithAuth(req, res, cb) { + var credentials = getCredentials(req); + var auth = { + copayerId: credentials.copayerId, + message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body), + signature: credentials.signature, + }; + CopayServer.getInstanceWithAuth(auth, function(err, server) { + if (err) return returnError(err, res, req); + return cb(server); + }); + }; + + router.post('/v1/wallets/', function(req, res) { + var server = CopayServer.getInstance(); + server.createWallet(req.body, function(err, walletId) { + if (err) return returnError(err, res, req); + + res.json({ + walletId: walletId, + }); + }); + }); + + router.post('/v1/wallets/:id/copayers/', function(req, res) { + req.body.walletId = req.params['id']; + var server = CopayServer.getInstance(); + server.joinWallet(req.body, function(err, result) { + if (err) return returnError(err, res, req); + + res.json(result); + }); + }); + + router.get('/v1/wallets/', function(req, res) { + getServerWithAuth(req, res, function(server) { + var result = {}; + async.parallel([ + + function(next) { + server.getWallet({}, function(err, wallet) { + if (err) return next(err); + result.wallet = wallet; + next(); + }); + }, + function(next) { + server.getBalance({}, function(err, balance) { + if (err) return next(err); + result.balance = balance; + next(); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, pendingTxps) { + if (err) return next(err); + result.pendingTxps = pendingTxps; + next(); + }); + }, + ], function(err) { + if (err) return returnError(err, res, req); + res.json(result); + }); + }); + }); + + + router.get('/v1/txproposals/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.getPendingTxs({}, function(err, pendings) { + if (err) return returnError(err, res, req); + res.json(pendings); + }); + }); + }); + + + router.post('/v1/txproposals/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.createTx(req.body, function(err, txp) { + if (err) return returnError(err, res, req); + res.json(txp); + }); + }); + }); + + + router.post('/v1/addresses/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.createAddress(req.body, function(err, address) { + if (err) return returnError(err, res, req); + res.json(address); + }); + }); + }); + + router.get('/v1/addresses/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.getAddresses({}, function(err, addresses) { + if (err) return returnError(err, res, req); + res.json(addresses); + }); + }); + }); + + router.get('/v1/balance/', function(req, res) { + getServerWithAuth(req, res, function(server) { + server.getBalance({}, function(err, balance) { + if (err) return returnError(err, res, req); + res.json(balance); + }); + }); + }); + + router.post('/v1/txproposals/:id/signatures/', function(req, res) { + getServerWithAuth(req, res, function(server) { + req.body.txProposalId = req.params['id']; + server.signTx(req.body, function(err, txp) { + if (err) return returnError(err, res, req); + res.json(txp); + res.end(); + }); + }); + }); + + // TODO Check HTTP verb and URL name + router.post('/v1/txproposals/:id/broadcast/', function(req, res) { + getServerWithAuth(req, res, function(server) { + req.body.txProposalId = req.params['id']; + server.broadcastTx(req.body, function(err, txp) { + if (err) return returnError(err, res, req); + res.json(txp); + res.end(); + }); + }); + }); + + router.post('/v1/txproposals/:id/rejections', function(req, res) { + getServerWithAuth(req, res, function(server) { + req.body.txProposalId = req.params['id']; + server.rejectTx(req.body, function(err, txp) { + if (err) return returnError(err, res, req); + res.json(txp); + res.end(); + }); + }); + }); + + router.delete('/v1/txproposals/:id/', function(req, res) { + getServerWithAuth(req, res, function(server) { + req.body.txProposalId = req.params['id']; + server.removePendingTx(req.body, function(err) { + if (err) return returnError(err, res, req); + res.end(); + }); + }); + }); + + // TODO: DEBUG only! + router.get('/v1/dump', function(req, res) { + var server = CopayServer.getInstance(); + server.storage._dump(function() { + res.end(); + }); + }); + app.use(opts.base_path || '/copay/api', router); + return app; +}; + +module.exports = ExpressApp; diff --git a/package.json b/package.json index bcca48e..049b877 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "mocha": "^1.18.2", "sinon": "^1.10.3", "memdown": "^1.0.0", - "jsdoc": "^3.3.0" + "jsdoc": "^3.3.0", + "supertest": "*" }, "scripts": { "start": "node server.js" diff --git a/test/integration/clientApi.js b/test/integration/clientApi.js index dea1aff..6c6e84a 100644 --- a/test/integration/clientApi.js +++ b/test/integration/clientApi.js @@ -4,14 +4,19 @@ var _ = require('lodash'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); +var levelup = require('levelup'); +var memdown = require('memdown'); +var request = require('supertest'); var Client = require('../../lib/client'); var API = Client.API; var Bitcore = require('bitcore'); var TestData = require('./clienttestdata'); var WalletUtils = require('../../lib/walletutils'); +var ExpressApp = require('../../lib/expressapp'); +var Storage = require('../../lib/storage'); describe('client API ', function() { - var client; + var client, app; beforeEach(function() { var fsmock = {};; @@ -24,10 +29,40 @@ describe('client API ', function() { client = new Client({ storage: storage }); + + var db = levelup(memdown, { + valueEncoding: 'json' + }); + var storage = new Storage({ + db: db + }); + app = ExpressApp.start({ + CopayServer: { + storage: storage + } + }); }); + var helpers = {}; + + helpers.request = function(args) { + if (args.method == 'get') { + request(app) + .get(relUrl) + .end(cb); + } else { + request(app) + .post(relUrl) + .send(body) + .end(function(err, res) { + console.log('[clientApi.js.59:err:]', err, res); //TODO + return cb(err, res); + }); + } + }; + describe('#_tryToComplete ', function() { - it('should complete a wallet ', function(done) { + it.only('should complete a wallet ', function(done) { var request = sinon.stub(); // Wallet request