commit
83ae3a7069
250
app.js
250
app.js
|
@ -1,252 +1,10 @@
|
||||||
'use strict';
|
#!/usr/bin/env node
|
||||||
|
|
||||||
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'));
|
|
||||||
|
|
||||||
|
var ExpressApp = require('./lib/expressapp');
|
||||||
|
|
||||||
var port = process.env.COPAY_PORT || 3001;
|
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);
|
app.listen(port);
|
||||||
|
|
||||||
console.log('Copay service running on port ' + port);
|
console.log('Copay service running on port ' + port);
|
||||||
|
|
|
@ -35,7 +35,8 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not signed pub keys
|
// Not signed pub keys
|
||||||
if (!WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) {
|
if (!copayer.xPubKey || !copayer.xPubKeySignature ||
|
||||||
|
!WalletUtils.verifyMessage(copayer.xPubKey, copayer.xPubKeySignature, walletPubKey)) {
|
||||||
log.error('Invalid signatures in server response');
|
log.error('Invalid signatures in server response');
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
|
@ -53,11 +54,12 @@ Verifier.checkCopayers = function(copayers, walletPrivKey, myXPrivKey, n) {
|
||||||
|
|
||||||
|
|
||||||
Verifier.checkTxProposal = function(data, txp) {
|
Verifier.checkTxProposal = function(data, txp) {
|
||||||
|
$.checkArgument(txp.creatorId);
|
||||||
|
|
||||||
var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) {
|
var creatorXPubKey = _.find(data.publicKeyRing, function(xPubKey) {
|
||||||
if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true;
|
if (WalletUtils.xPubToCopayerId(xPubKey) === txp.creatorId) return true;
|
||||||
});
|
});
|
||||||
if (!creatorXPubKey) return false;
|
if (!creatorXPubKey) return false;
|
||||||
|
|
||||||
var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/0').publicKey.toString();
|
var creatorSigningPubKey = (new Bitcore.HDPublicKey(creatorXPubKey)).derive('m/1/0').publicKey.toString();
|
||||||
|
|
||||||
var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.message);
|
var hash = WalletUtils.getProposalHash(txp.toAddress, txp.amount, txp.message);
|
||||||
|
|
|
@ -60,8 +60,11 @@ function API(opts) {
|
||||||
this.verbose = !!opts.verbose;
|
this.verbose = !!opts.verbose;
|
||||||
this.request = request || opts.request;
|
this.request = request || opts.request;
|
||||||
this.baseUrl = opts.baseUrl || BASE_URL;
|
this.baseUrl = opts.baseUrl || BASE_URL;
|
||||||
|
this.basePath = this.baseUrl.replace(/http.?:\/\/[a-zA-Z0-9:-]*\//,'/');
|
||||||
if (this.verbose) {
|
if (this.verbose) {
|
||||||
log.level = 'debug';
|
log.level = 'debug';
|
||||||
|
} else {
|
||||||
|
log.level = 'info';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -127,6 +130,8 @@ API.prototype._doRequest = function(method, url, args, data, cb) {
|
||||||
|
|
||||||
var absUrl = this.baseUrl + url;
|
var absUrl = this.baseUrl + url;
|
||||||
var args = {
|
var args = {
|
||||||
|
// relUrl: only for testing with `supertest`
|
||||||
|
relUrl: this.basePath + url,
|
||||||
headers: {
|
headers: {
|
||||||
'x-identity': data.copayerId,
|
'x-identity': data.copayerId,
|
||||||
'x-signature': reqSignature,
|
'x-signature': reqSignature,
|
||||||
|
@ -415,7 +420,7 @@ API.prototype.import = function(str, cb) {
|
||||||
|
|
||||||
var xPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString();
|
var xPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString();
|
||||||
|
|
||||||
data.publicKeyRing.push(xPubKey);
|
data.publicKeyRing.unshift(xPubKey);
|
||||||
data.copayerId = WalletUtils.xPubToCopayerId(xPubKey);
|
data.copayerId = WalletUtils.xPubToCopayerId(xPubKey);
|
||||||
data.n = data.publicKeyRing.length;
|
data.n = data.publicKeyRing.length;
|
||||||
data.signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey.toWIF();
|
data.signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey.toWIF();
|
||||||
|
@ -444,6 +449,7 @@ API.prototype.getTxProposals = function(opts, cb) {
|
||||||
|
|
||||||
API.prototype.signTxProposal = function(txp, cb) {
|
API.prototype.signTxProposal = function(txp, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
$.checkArgument(txp.creatorId);
|
||||||
|
|
||||||
this._loadAndCheck(function(err, data) {
|
this._loadAndCheck(function(err, data) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
@ -452,7 +458,6 @@ API.prototype.signTxProposal = function(txp, cb) {
|
||||||
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
|
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Derive proper key to sign, for each input
|
//Derive proper key to sign, for each input
|
||||||
var privs = [],
|
var privs = [],
|
||||||
derived = {};
|
derived = {};
|
||||||
|
@ -493,6 +498,7 @@ API.prototype.signTxProposal = function(txp, cb) {
|
||||||
|
|
||||||
API.prototype.rejectTxProposal = function(txp, reason, cb) {
|
API.prototype.rejectTxProposal = function(txp, reason, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
$.checkArgument(cb);
|
||||||
|
|
||||||
this._loadAndCheck(
|
this._loadAndCheck(
|
||||||
function(err, data) {
|
function(err, data) {
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
'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);
|
||||||
|
if (!credentials)
|
||||||
|
return returnError(new CopayServer.ClientError({
|
||||||
|
code: 'NOTAUTHORIZED'
|
||||||
|
}), res, 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;
|
|
@ -26,15 +26,18 @@ var TxProposal = require('./model/txproposal');
|
||||||
var Notification = require('./model/notification');
|
var Notification = require('./model/notification');
|
||||||
|
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
var storage;
|
var storage, blockExplorer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of the Copay server.
|
* Creates an instance of the Copay server.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function CopayServer() {
|
function CopayServer() {
|
||||||
if (!initialized) throw new Error('Server not initialized');
|
if (!initialized)
|
||||||
|
throw new Error('Server not initialized');
|
||||||
|
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
|
this.blockExplorer = blockExplorer;
|
||||||
this.notifyTicker = 0;
|
this.notifyTicker = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,10 +48,12 @@ nodeutil.inherits(CopayServer, events.EventEmitter);
|
||||||
* Initializes global settings for all instances.
|
* Initializes global settings for all instances.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
* @param {Storage} [opts.storage] - The storage provider.
|
* @param {Storage} [opts.storage] - The storage provider.
|
||||||
|
* @param {Storage} [opts.blockExplorer] - The blockExporer provider.
|
||||||
*/
|
*/
|
||||||
CopayServer.initialize = function(opts) {
|
CopayServer.initialize = function(opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
storage = opts.storage || new Storage();
|
storage = opts.storage || new Storage();
|
||||||
|
blockExplorer = opts.blockExplorer;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -311,6 +316,9 @@ CopayServer.prototype.verifyMessageSignature = function(opts, cb) {
|
||||||
CopayServer.prototype._getBlockExplorer = function(provider, network) {
|
CopayServer.prototype._getBlockExplorer = function(provider, network) {
|
||||||
var url;
|
var url;
|
||||||
|
|
||||||
|
if (this.blockExplorer)
|
||||||
|
return this.blockExplorer;
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
default:
|
default:
|
||||||
case 'insight':
|
case 'insight':
|
||||||
|
@ -352,7 +360,6 @@ CopayServer.prototype._getUtxos = function(cb) {
|
||||||
var utxos = _.map(inutxos, function(i) {
|
var utxos = _.map(inutxos, function(i) {
|
||||||
return i.toObject();
|
return i.toObject();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.getPendingTxs({}, function(err, txps) {
|
self.getPendingTxs({}, function(err, txps) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
@ -773,7 +780,7 @@ CopayServer.prototype.rejectTx = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return cb();
|
return cb(null, txp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
var $ = require('preconditions').singleton();
|
||||||
var sjcl = require('sjcl');
|
var sjcl = require('sjcl');
|
||||||
|
|
||||||
var Bitcore = require('bitcore');
|
var Bitcore = require('bitcore');
|
||||||
|
@ -14,6 +15,7 @@ function WalletUtils() {};
|
||||||
/* TODO: It would be nice to be compatible with bitcoind signmessage. How
|
/* TODO: It would be nice to be compatible with bitcoind signmessage. How
|
||||||
* the hash is calculated there? */
|
* the hash is calculated there? */
|
||||||
WalletUtils.hashMessage = function(text) {
|
WalletUtils.hashMessage = function(text) {
|
||||||
|
$.checkArgument(text);
|
||||||
var buf = new Buffer(text);
|
var buf = new Buffer(text);
|
||||||
var ret = crypto.Hash.sha256sha256(buf);
|
var ret = crypto.Hash.sha256sha256(buf);
|
||||||
ret = new Bitcore.encoding.BufferReader(ret).readReverse();
|
ret = new Bitcore.encoding.BufferReader(ret).readReverse();
|
||||||
|
@ -22,6 +24,7 @@ WalletUtils.hashMessage = function(text) {
|
||||||
|
|
||||||
|
|
||||||
WalletUtils.signMessage = function(text, privKey) {
|
WalletUtils.signMessage = function(text, privKey) {
|
||||||
|
$.checkArgument(text);
|
||||||
var priv = new PrivateKey(privKey);
|
var priv = new PrivateKey(privKey);
|
||||||
var hash = WalletUtils.hashMessage(text);
|
var hash = WalletUtils.hashMessage(text);
|
||||||
return crypto.ECDSA.sign(hash, priv, 'little').toString();
|
return crypto.ECDSA.sign(hash, priv, 'little').toString();
|
||||||
|
@ -29,6 +32,11 @@ WalletUtils.signMessage = function(text, privKey) {
|
||||||
|
|
||||||
|
|
||||||
WalletUtils.verifyMessage = function(text, signature, pubKey) {
|
WalletUtils.verifyMessage = function(text, signature, pubKey) {
|
||||||
|
$.checkArgument(text, pubKey);
|
||||||
|
|
||||||
|
if (!signature)
|
||||||
|
return false;
|
||||||
|
|
||||||
var pub = new PublicKey(pubKey);
|
var pub = new PublicKey(pubKey);
|
||||||
var hash = WalletUtils.hashMessage(text);
|
var hash = WalletUtils.hashMessage(text);
|
||||||
|
|
||||||
|
@ -69,6 +77,7 @@ WalletUtils.toSecret = function(walletId, walletPrivKey, network) {
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletUtils.fromSecret = function(secret) {
|
WalletUtils.fromSecret = function(secret) {
|
||||||
|
$.checkArgument(secret);
|
||||||
var secretSplit = secret.split(':');
|
var secretSplit = secret.split(':');
|
||||||
var walletId = secretSplit[0];
|
var walletId = secretSplit[0];
|
||||||
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]);
|
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]);
|
||||||
|
@ -104,8 +113,10 @@ WalletUtils.UNITS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletUtils.parseAmount = function(text) {
|
WalletUtils.parseAmount = function(text) {
|
||||||
var regex = '^(\\d*(\\.\\d{0,8})?)\\s*(' + _.keys(WalletUtils.UNITS).join('|') + ')?$';
|
if (!_.isString(text))
|
||||||
|
text = text.toString();
|
||||||
|
|
||||||
|
var regex = '^(\\d*(\\.\\d{0,8})?)\\s*(' + _.keys(WalletUtils.UNITS).join('|') + ')?$';
|
||||||
var match = new RegExp(regex, 'i').exec(text.trim());
|
var match = new RegExp(regex, 'i').exec(text.trim());
|
||||||
|
|
||||||
if (!match || match.length === 0) throw new Error('Invalid amount');
|
if (!match || match.length === 0) throw new Error('Invalid amount');
|
||||||
|
|
|
@ -40,7 +40,8 @@
|
||||||
"mocha": "^1.18.2",
|
"mocha": "^1.18.2",
|
||||||
"sinon": "^1.10.3",
|
"sinon": "^1.10.3",
|
||||||
"memdown": "^1.0.0",
|
"memdown": "^1.0.0",
|
||||||
"jsdoc": "^3.3.0"
|
"jsdoc": "^3.3.0",
|
||||||
|
"supertest": "*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
|
|
|
@ -4,133 +4,497 @@ var _ = require('lodash');
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var sinon = require('sinon');
|
var sinon = require('sinon');
|
||||||
var should = chai.should();
|
var should = chai.should();
|
||||||
|
var levelup = require('levelup');
|
||||||
|
var memdown = require('memdown');
|
||||||
|
var async = require('async');
|
||||||
|
var request = require('supertest');
|
||||||
var Client = require('../../lib/client');
|
var Client = require('../../lib/client');
|
||||||
var API = Client.API;
|
var API = Client.API;
|
||||||
var Bitcore = require('bitcore');
|
var Bitcore = require('bitcore');
|
||||||
var TestData = require('./clienttestdata');
|
var TestData = require('./clienttestdata');
|
||||||
var WalletUtils = require('../../lib/walletutils');
|
var WalletUtils = require('../../lib/walletutils');
|
||||||
|
var ExpressApp = require('../../lib/expressapp');
|
||||||
|
var Storage = require('../../lib/storage');
|
||||||
|
|
||||||
|
|
||||||
|
var helpers = {};
|
||||||
|
|
||||||
|
helpers.getRequest = function(app) {
|
||||||
|
return function(args, cb) {
|
||||||
|
var req = request(app);
|
||||||
|
var r = req[args.method](args.relUrl);
|
||||||
|
|
||||||
|
if (args.headers) {
|
||||||
|
_.each(args.headers, function(v, k) {
|
||||||
|
r.set(k, v);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!_.isEmpty(args.body)) {
|
||||||
|
r.send(args.body);
|
||||||
|
};
|
||||||
|
r.end(function(err, res) {
|
||||||
|
return cb(err, res, res.body);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.createAndJoinWallet = function(clients, m, n, cb) {
|
||||||
|
clients[0].createWallet('wallet name', 'creator', m, n, 'testnet',
|
||||||
|
function(err, secret) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (n == 1) return cb();
|
||||||
|
|
||||||
|
should.exist(secret);
|
||||||
|
async.each(_.range(n - 1), function(i, cb) {
|
||||||
|
clients[i + 1].joinWallet(secret, 'copayer ' + (i + 1), function(err, result) {
|
||||||
|
should.not.exist(err);
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
return cb(null, {
|
||||||
|
m: m,
|
||||||
|
n: n,
|
||||||
|
secret: secret,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var fsmock = {};
|
||||||
|
var content = {};
|
||||||
|
fsmock.readFile = function(name, enc, cb) {
|
||||||
|
if (!content || _.isEmpty(content[name]))
|
||||||
|
return cb('empty');
|
||||||
|
|
||||||
|
return cb(null, content[name]);
|
||||||
|
};
|
||||||
|
fsmock.writeFile = function(name, data, cb) {
|
||||||
|
content[name] = data;
|
||||||
|
return cb();
|
||||||
|
};
|
||||||
|
fsmock.reset = function() {
|
||||||
|
content = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
fsmock._get = function(name) {
|
||||||
|
return content[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var blockExplorerMock = {};
|
||||||
|
blockExplorerMock.utxos = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
blockExplorerMock.getUnspentUtxos = function(dummy, cb) {
|
||||||
|
var ret = _.map(blockExplorerMock.utxos || [], function(x) {
|
||||||
|
x.toObject = function() {
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
return cb(null, ret);
|
||||||
|
};
|
||||||
|
|
||||||
|
blockExplorerMock.setUtxo = function(address, amount, m) {
|
||||||
|
blockExplorerMock.utxos.push({
|
||||||
|
txid: Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex'),
|
||||||
|
vout: Math.floor((Math.random() * 10) + 1),
|
||||||
|
amount: amount,
|
||||||
|
address: address.address,
|
||||||
|
scriptPubKey: Bitcore.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut().toString(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
blockExplorerMock.broadcast = function(raw, cb) {
|
||||||
|
blockExplorerMock.lastBroadcasted = raw;
|
||||||
|
return cb(null, (new Bitcore.Transaction(raw)).id);
|
||||||
|
};
|
||||||
|
|
||||||
|
blockExplorerMock.reset = function() {
|
||||||
|
blockExplorerMock.utxos = [];
|
||||||
|
};
|
||||||
|
|
||||||
describe('client API ', function() {
|
describe('client API ', function() {
|
||||||
var client;
|
var clients, app;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
var fsmock = {};;
|
clients = [];
|
||||||
fsmock.readFile = sinon.mock().yields(null, JSON.stringify(TestData.storage.wallet11));
|
var db = levelup(memdown, {
|
||||||
fsmock.writeFile = sinon.mock().yields();
|
valueEncoding: 'json'
|
||||||
|
});
|
||||||
|
var storage = new Storage({
|
||||||
|
db: db
|
||||||
|
});
|
||||||
|
app = ExpressApp.start({
|
||||||
|
CopayServer: {
|
||||||
|
storage: storage,
|
||||||
|
blockExplorer: blockExplorerMock,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Generates 5 clients
|
||||||
|
_.each(_.range(5), function(i) {
|
||||||
var storage = new Client.FileStorage({
|
var storage = new Client.FileStorage({
|
||||||
filename: 'dummy',
|
filename: 'client' + i,
|
||||||
fs: fsmock,
|
fs: fsmock,
|
||||||
});
|
});
|
||||||
client = new Client({
|
var client = new Client({
|
||||||
storage: storage
|
storage: storage,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#_tryToComplete ', function() {
|
client.request = helpers.getRequest(app);
|
||||||
it('should complete a wallet ', function(done) {
|
clients.push(client);
|
||||||
var request = sinon.stub();
|
});
|
||||||
|
fsmock.reset();
|
||||||
|
blockExplorerMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
// Wallet request
|
describe('Wallet Creation', function() {
|
||||||
request.onCall(0).yields(null, {
|
it('should check balance in a 1-1 ', function(done) {
|
||||||
statusCode: 200,
|
helpers.createAndJoinWallet(clients, 1, 1, function(err) {
|
||||||
}, TestData.serverResponse.completeWallet);
|
should.not.exist(err);
|
||||||
request.onCall(1).yields(null, {
|
clients[0].getBalance(function(err, x) {
|
||||||
statusCode: 200,
|
|
||||||
}, "pepe");
|
|
||||||
|
|
||||||
client.request = request;
|
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
|
|
||||||
client.getBalance(function(err, x) {
|
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
done();
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should be able to complete wallets in copayer that joined later', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[0].getBalance(function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[1].getBalance(function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[2].getBalance(function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not allow to join a full wallet ', function(done) {
|
||||||
it('should handle incomple wallets', function(done) {
|
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
|
||||||
var request = sinon.stub();
|
should.not.exist(err);
|
||||||
|
should.exist(w.secret);
|
||||||
// Wallet request
|
clients[4].joinWallet(w.secret, 'copayer', function(err, result) {
|
||||||
request.onCall(0).yields(null, {
|
err.should.contain('Request error');
|
||||||
statusCode: 200,
|
done();
|
||||||
}, TestData.serverResponse.incompleteWallet);
|
});
|
||||||
|
});
|
||||||
client.request = request;
|
});
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
|
it('should fail with a unknown secret', function(done) {
|
||||||
client.createAddress(function(err, x) {
|
var oldSecret = '3f8e5acb-ceeb-4aae-134f-692d934e3b1c:L2gohj8s2fLKqVU5cQutAVGciutUxczFxLxxXHFsjzLh71ZjkFQQ:T';
|
||||||
err.should.contain('Incomplete');
|
clients[0].joinWallet(oldSecret, 'copayer', function(err, result) {
|
||||||
|
err.should.contain('Request error');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject wallets with bad signatures', function(done) {
|
it('should reject wallets with bad signatures', function(done) {
|
||||||
var request = sinon.stub();
|
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
|
||||||
// Wallet request
|
should.not.exist(err);
|
||||||
request.onCall(0).yields(null, {
|
|
||||||
statusCode: 200,
|
|
||||||
}, TestData.serverResponse.corruptWallet22);
|
|
||||||
|
|
||||||
client.request = request;
|
// Get right response
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
|
var data = clients[0]._load(function(err, data) {
|
||||||
client.createAddress(function(err, x) {
|
var url = '/v1/wallets/';
|
||||||
|
clients[0]._doGetRequest(url, data, function(err, x) {
|
||||||
|
|
||||||
|
// Tamper data
|
||||||
|
x.wallet.copayers[0].xPubKey = x.wallet.copayers[1].xPubKey;
|
||||||
|
|
||||||
|
// Tamper response
|
||||||
|
clients[1]._doGetRequest = sinon.stub().yields(null, x);
|
||||||
|
|
||||||
|
clients[1].getBalance(function(err, x) {
|
||||||
err.should.contain('verified');
|
err.should.contain('verified');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should reject wallets with missing signatures ', function(done) {
|
it('should reject wallets with missing signatures', function(done) {
|
||||||
var request = sinon.stub();
|
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
|
||||||
// Wallet request
|
should.not.exist(err);
|
||||||
request.onCall(0).yields(null, {
|
|
||||||
statusCode: 200,
|
|
||||||
}, TestData.serverResponse.corruptWallet222);
|
|
||||||
|
|
||||||
client.request = request;
|
// Get right response
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
|
var data = clients[0]._load(function(err, data) {
|
||||||
client.createAddress(function(err, x) {
|
var url = '/v1/wallets/';
|
||||||
|
clients[0]._doGetRequest(url, data, function(err, x) {
|
||||||
|
|
||||||
|
// Tamper data
|
||||||
|
delete x.wallet.copayers[1].xPubKey;
|
||||||
|
|
||||||
|
// Tamper response
|
||||||
|
clients[1]._doGetRequest = sinon.stub().yields(null, x);
|
||||||
|
|
||||||
|
clients[1].getBalance(function(err, x) {
|
||||||
err.should.contain('verified');
|
err.should.contain('verified');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should reject wallets missing caller"s pubkey', function(done) {
|
it('should reject wallets missing caller"s pubkey', function(done) {
|
||||||
var request = sinon.stub();
|
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
|
||||||
// Wallet request
|
should.not.exist(err);
|
||||||
request.onCall(0).yields(null, {
|
|
||||||
statusCode: 200,
|
|
||||||
}, TestData.serverResponse.missingMyPubKey);
|
|
||||||
|
|
||||||
client.request = request;
|
// Get right response
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.incompleteWallet22));
|
var data = clients[0]._load(function(err, data) {
|
||||||
client.createAddress(function(err, x) {
|
var url = '/v1/wallets/';
|
||||||
|
clients[0]._doGetRequest(url, data, function(err, x) {
|
||||||
|
|
||||||
|
// Tamper data. Replace caller's pubkey
|
||||||
|
x.wallet.copayers[1].xPubKey = (new Bitcore.HDPrivateKey()).publicKey;
|
||||||
|
// Add a correct signature
|
||||||
|
x.wallet.copayers[1].xPubKeySignature = WalletUtils.signMessage(
|
||||||
|
x.wallet.copayers[1].xPubKey, data.walletPrivKey),
|
||||||
|
|
||||||
|
// Tamper response
|
||||||
|
clients[1]._doGetRequest = sinon.stub().yields(null, x);
|
||||||
|
|
||||||
|
clients[1].getBalance(function(err, x) {
|
||||||
err.should.contain('verified');
|
err.should.contain('verified');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
describe('#createAddress ', function() {
|
});
|
||||||
it('should check address ', function(done) {
|
});
|
||||||
|
|
||||||
var response = {
|
|
||||||
createdOn: 1424105995,
|
|
||||||
address: '2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq',
|
|
||||||
path: 'm/2147483647/0/7',
|
|
||||||
publicKeys: ['03f6a5fe8db51bfbaf26ece22a3e3bc242891a47d3048fc70bc0e8c03a071ad76f']
|
|
||||||
};
|
|
||||||
var request = sinon.mock().yields(null, {
|
|
||||||
statusCode: 200
|
|
||||||
}, response);
|
|
||||||
client.request = request;
|
|
||||||
|
|
||||||
|
|
||||||
client.createAddress(function(err, x) {
|
describe('Address Creation', function() {
|
||||||
|
it('should be able to create address in all copayers in a 2-3 wallet', function(done) {
|
||||||
|
this.timeout(5000);
|
||||||
|
helpers.createAndJoinWallet(clients, 2, 3, function(err) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
x.address.should.equal('2N3fA6wDtnebzywPkGuNK9KkFaEzgbPRRTq');
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
clients[1].createAddress(function(err, x1) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x1.address);
|
||||||
|
clients[2].createAddress(function(err, x2) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x2.address);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should see balance on address created by others', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
|
||||||
|
blockExplorerMock.setUtxo(x0, 10, w.m);
|
||||||
|
clients[0].getBalance(function(err, bal0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
bal0.totalAmount.should.equal(10 * 1e8);
|
||||||
|
bal0.lockedAmount.should.equal(0);
|
||||||
|
clients[1].getBalance(function(err, bal1) {
|
||||||
|
bal1.totalAmount.should.equal(10 * 1e8);
|
||||||
|
bal1.lockedAmount.should.equal(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Wallet Backups and Mobility', function() {
|
||||||
|
|
||||||
|
it('round trip #import #export', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 2, 2, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[0].export(function(err, str) {
|
||||||
|
should.not.exist(err);
|
||||||
|
var original = JSON.parse(fsmock._get('client0'));
|
||||||
|
clients[2].import(str, function(err, wallet) {
|
||||||
|
should.not.exist(err);
|
||||||
|
var clone = JSON.parse(fsmock._get('client2'));
|
||||||
|
delete original.walletPrivKey; // no need to persist it.
|
||||||
|
clone.should.deep.equal(original);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should recreate a wallet, create addresses and receive money', function(done) {
|
||||||
|
var backup = '["tprv8ZgxMBicQKsPehCdj4HM1MZbKVXBFt5Dj9nQ44M99EdmdiUfGtQBDTSZsKmzdUrB1vEuP6ipuoa39UXwPS2CvnjE1erk5aUjc5vQZkWvH4B",2,["tpubD6NzVbkrYhZ4XCNDPDtyRWPxvJzvTkvUE2cMPB8jcUr9Dkicv6cYQmA18DBAid6eRK1BGCU9nzgxxVdQUGLYJ34XsPXPW4bxnH4PH6oQBF3"],"sd0kzXmlXBgTGHrKaBW4aA=="]';
|
||||||
|
clients[0].import(backup, function(err, wallet) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[0].reCreateWallet('pepe', function(err, wallet) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
blockExplorerMock.setUtxo(x0, 10, 2);
|
||||||
|
clients[0].getBalance(function(err, bal0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
bal0.totalAmount.should.equal(10 * 1e8);
|
||||||
|
bal0.lockedAmount.should.equal(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Send Transactions', function() {
|
||||||
|
it('Send and broadcast in 1-1 wallet', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 1, 1, function(err, w) {
|
||||||
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
blockExplorerMock.setUtxo(x0, 1, 1);
|
||||||
|
var opts = {
|
||||||
|
amount: '0.1btc',
|
||||||
|
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
|
||||||
|
message: 'hola 1-1',
|
||||||
|
};
|
||||||
|
clients[0].sendTxProposal(opts, function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
x.requiredRejections.should.equal(1);
|
||||||
|
x.requiredSignatures.should.equal(1);
|
||||||
|
x.status.should.equal('pending');
|
||||||
|
x.changeAddress.path.should.equal('m/2147483647/1/0');
|
||||||
|
clients[0].signTxProposal(x, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
tx.status.should.equal('broadcasted');
|
||||||
|
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Send and broadcast in 2-3 wallet', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 2, 3, function(err, w) {
|
||||||
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
blockExplorerMock.setUtxo(x0, 10, 1);
|
||||||
|
var opts = {
|
||||||
|
amount: 10000,
|
||||||
|
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
|
||||||
|
message: 'hola 1-1',
|
||||||
|
};
|
||||||
|
clients[0].sendTxProposal(opts, function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
x.status.should.equal('pending');
|
||||||
|
x.requiredRejections.should.equal(2);
|
||||||
|
x.requiredSignatures.should.equal(2);
|
||||||
|
clients[0].signTxProposal(x, function(err, tx) {
|
||||||
|
should.not.exist(err, err);
|
||||||
|
tx.status.should.equal('pending');
|
||||||
|
clients[1].signTxProposal(x, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
tx.status.should.equal('broadcasted');
|
||||||
|
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Send, reject, 2 signs and broadcast in 2-3 wallet', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 2, 3, function(err, w) {
|
||||||
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
blockExplorerMock.setUtxo(x0, 10, 1);
|
||||||
|
var opts = {
|
||||||
|
amount: 10000,
|
||||||
|
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
|
||||||
|
message: 'hola 1-1',
|
||||||
|
};
|
||||||
|
clients[0].sendTxProposal(opts, function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
x.status.should.equal('pending');
|
||||||
|
x.requiredRejections.should.equal(2);
|
||||||
|
x.requiredSignatures.should.equal(2);
|
||||||
|
clients[0].rejectTxProposal(x, 'no me gusto', function(err, tx) {
|
||||||
|
should.not.exist(err, err);
|
||||||
|
tx.status.should.equal('pending');
|
||||||
|
clients[1].signTxProposal(x, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
clients[2].signTxProposal(x, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
tx.status.should.equal('broadcasted');
|
||||||
|
tx.txid.should.equal((new Bitcore.Transaction(blockExplorerMock.lastBroadcasted)).id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Send, reject in 3-4 wallet', function(done) {
|
||||||
|
helpers.createAndJoinWallet(clients, 3, 4, function(err, w) {
|
||||||
|
clients[0].createAddress(function(err, x0) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(x0.address);
|
||||||
|
blockExplorerMock.setUtxo(x0, 10, 1);
|
||||||
|
var opts = {
|
||||||
|
amount: 10000,
|
||||||
|
toAddress: 'n2TBMPzPECGUfcT2EByiTJ12TPZkhN2mN5',
|
||||||
|
message: 'hola 1-1',
|
||||||
|
};
|
||||||
|
clients[0].sendTxProposal(opts, function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
x.status.should.equal('pending');
|
||||||
|
x.requiredRejections.should.equal(2);
|
||||||
|
x.requiredSignatures.should.equal(3);
|
||||||
|
|
||||||
|
clients[0].rejectTxProposal(x, 'no me gusto', function(err, tx) {
|
||||||
|
should.not.exist(err, err);
|
||||||
|
tx.status.should.equal('pending');
|
||||||
|
clients[1].signTxProposal(x, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
tx.status.should.equal('pending');
|
||||||
|
clients[2].rejectTxProposal(x, 'tampoco me gusto', function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
tx.status.should.equal('rejected');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
describe('TODO', function(x) {
|
||||||
it('should detect fake addresses ', function(done) {
|
it('should detect fake addresses ', function(done) {
|
||||||
var response = {
|
var response = {
|
||||||
createdOn: 1424105995,
|
createdOn: 1424105995,
|
||||||
|
@ -151,25 +515,6 @@ describe('client API ', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('#export & #import 2-2 wallet', function() {
|
|
||||||
it('round trip ', function(done) {
|
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete22));
|
|
||||||
client.export(function(err, str) {
|
|
||||||
should.not.exist(err);
|
|
||||||
|
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null);
|
|
||||||
client.import(str, function(err, wallet) {
|
|
||||||
should.not.exist(err);
|
|
||||||
var wallet = JSON.parse(client.storage.fs.writeFile.getCall(0).args[1]);
|
|
||||||
TestData.storage.complete22.should.deep.equal(wallet);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('#getTxProposals', function() {
|
describe('#getTxProposals', function() {
|
||||||
it('should return tx proposals and decrypt message', function(done) {
|
it('should return tx proposals and decrypt message', function(done) {
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11));
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11));
|
||||||
|
@ -188,10 +533,6 @@ describe('client API ', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#recreate', function() {
|
|
||||||
it.skip('Should recreate a wallet acording stored data', function(done) {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#sendTxProposal ', function() {
|
describe('#sendTxProposal ', function() {
|
||||||
it('should send tx proposal with encrypted message', function(done) {
|
it('should send tx proposal with encrypted message', function(done) {
|
||||||
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11));
|
client.storage.fs.readFile = sinon.stub().yields(null, JSON.stringify(TestData.storage.complete11));
|
||||||
|
@ -261,4 +602,5 @@ describe('client API ', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,6 @@ var Copayer = require('../../lib/model/copayer');
|
||||||
var CopayServer = require('../../lib/server');
|
var CopayServer = require('../../lib/server');
|
||||||
var TestData = require('../testdata');
|
var TestData = require('../testdata');
|
||||||
|
|
||||||
|
|
||||||
var helpers = {};
|
var helpers = {};
|
||||||
helpers.getAuthServer = function(copayerId, cb) {
|
helpers.getAuthServer = function(copayerId, cb) {
|
||||||
var signatureStub = sinon.stub(CopayServer.prototype, '_verifySignature');
|
var signatureStub = sinon.stub(CopayServer.prototype, '_verifySignature');
|
||||||
|
|
Loading…
Reference in New Issue