Merge pull request #669 from isocolsky/feat/support
Add API methods for support staff
This commit is contained in:
commit
d416284fc3
|
@ -39,7 +39,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
this.app.use(function(req, res, next) {
|
this.app.use(function(req, res, next) {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,X-Requested-With,Content-Type,Authorization');
|
res.setHeader('Access-Control-Allow-Headers', 'x-signature,x-identity,x-session,x-client-version,x-wallet-id,X-Requested-With,Content-Type,Authorization');
|
||||||
res.setHeader('x-service-version', WalletService.getServiceVersion());
|
res.setHeader('x-service-version', WalletService.getServiceVersion());
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -100,7 +100,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
|
|
||||||
var status = (err.code == 'NOT_AUTHORIZED') ? 401 : 400;
|
var status = (err.code == 'NOT_AUTHORIZED') ? 401 : 400;
|
||||||
if (!opts.disableLogs)
|
if (!opts.disableLogs)
|
||||||
log.info('Client Err: ' + status + ' ' + req.url + ' ' + err);
|
log.info('Client Err: ' + status + ' ' + req.url + ' ' + JSON.stringify(err));
|
||||||
|
|
||||||
res.status(status).json({
|
res.status(status).json({
|
||||||
code: err.code,
|
code: err.code,
|
||||||
|
@ -165,6 +165,7 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body),
|
message: req.method.toLowerCase() + '|' + req.url + '|' + JSON.stringify(req.body),
|
||||||
signature: credentials.signature,
|
signature: credentials.signature,
|
||||||
clientVersion: req.header('x-client-version'),
|
clientVersion: req.header('x-client-version'),
|
||||||
|
walletId: req.header('x-wallet-id'),
|
||||||
};
|
};
|
||||||
if (opts.allowSession) {
|
if (opts.allowSession) {
|
||||||
auth.session = credentials.session;
|
auth.session = credentials.session;
|
||||||
|
@ -172,6 +173,12 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
WalletService.getInstanceWithAuth(auth, function(err, server) {
|
WalletService.getInstanceWithAuth(auth, function(err, server) {
|
||||||
if (err) return returnError(err, res, req);
|
if (err) return returnError(err, res, req);
|
||||||
|
|
||||||
|
if (opts.onlySupportStaff && !server.copayerIsSupportStaff) {
|
||||||
|
return returnError(new WalletService.ClientError({
|
||||||
|
code: 'NOT_AUTHORIZED'
|
||||||
|
}), res, req);
|
||||||
|
}
|
||||||
|
|
||||||
// For logging
|
// For logging
|
||||||
req.walletId = server.walletId;
|
req.walletId = server.walletId;
|
||||||
req.copayerId = server.copayerId;
|
req.copayerId = server.copayerId;
|
||||||
|
@ -300,6 +307,29 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/v1/wallets/:identifier/', function(req, res) {
|
||||||
|
getServerWithAuth(req, res, {
|
||||||
|
onlySupportStaff: true
|
||||||
|
}, function(server) {
|
||||||
|
var opts = {
|
||||||
|
identifier: req.params['identifier'],
|
||||||
|
};
|
||||||
|
server.getWalletFromIdentifier(opts, function(err, wallet) {
|
||||||
|
if (err) return returnError(err, res, req);
|
||||||
|
if (!wallet) return res.end();
|
||||||
|
|
||||||
|
server.walletId = wallet.id;
|
||||||
|
var opts = {};
|
||||||
|
if (req.query.includeExtendedInfo == '1') opts.includeExtendedInfo = true;
|
||||||
|
if (req.query.twoStep == '1') opts.twoStep = true;
|
||||||
|
server.getStatus(opts, function(err, status) {
|
||||||
|
if (err) return returnError(err, res, req);
|
||||||
|
res.json(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/v1/preferences/', function(req, res) {
|
router.get('/v1/preferences/', function(req, res) {
|
||||||
getServerWithAuth(req, res, function(server) {
|
getServerWithAuth(req, res, function(server) {
|
||||||
server.getPreferences({}, function(err, preferences) {
|
server.getPreferences({}, function(err, preferences) {
|
||||||
|
|
103
lib/server.js
103
lib/server.js
|
@ -200,6 +200,7 @@ WalletService.getInstance = function(opts) {
|
||||||
* @param {string} opts.signature - (Optional) Signature of message to be verified using one of the copayer's requestPubKeys. Only needed if no session token is provided.
|
* @param {string} opts.signature - (Optional) Signature of message to be verified using one of the copayer's requestPubKeys. Only needed if no session token is provided.
|
||||||
* @param {string} opts.session - (Optional) A valid session token previously obtained using the #login method
|
* @param {string} opts.session - (Optional) A valid session token previously obtained using the #login method
|
||||||
* @param {string} opts.clientVersion - A string that identifies the client issuing the request
|
* @param {string} opts.clientVersion - A string that identifies the client issuing the request
|
||||||
|
* @param {string} [opts.walletId] - The wallet id to use as current wallet for this request (only when copayer is support staff).
|
||||||
*/
|
*/
|
||||||
WalletService.getInstanceWithAuth = function(opts, cb) {
|
WalletService.getInstanceWithAuth = function(opts, cb) {
|
||||||
function withSignature(cb) {
|
function withSignature(cb) {
|
||||||
|
@ -216,12 +217,17 @@ WalletService.getInstanceWithAuth = function(opts, cb) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
|
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
|
||||||
|
|
||||||
var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
|
if (!copayer.isSupportStaff) {
|
||||||
if (!isValid)
|
var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
|
||||||
return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature'));
|
if (!isValid)
|
||||||
|
return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature'));
|
||||||
|
server.walletId = copayer.walletId;
|
||||||
|
} else {
|
||||||
|
server.walletId = opts.walletId || copayer.walletId;
|
||||||
|
server.copayerIsSupportStaff = true;
|
||||||
|
}
|
||||||
|
|
||||||
server.copayerId = opts.copayerId;
|
server.copayerId = opts.copayerId;
|
||||||
server.walletId = copayer.walletId;
|
|
||||||
return cb(null, server);
|
return cb(null, server);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -390,6 +396,71 @@ WalletService.prototype.getWallet = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a wallet from storage.
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.identifier - The identifier associated with the wallet (one of: walletId, address, txid).
|
||||||
|
* @returns {Object} wallet
|
||||||
|
*/
|
||||||
|
WalletService.prototype.getWalletFromIdentifier = function(opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!opts.identifier) return cb();
|
||||||
|
|
||||||
|
var walletId;
|
||||||
|
async.parallel([
|
||||||
|
|
||||||
|
function(done) {
|
||||||
|
self.storage.fetchWallet(opts.identifier, function(err, wallet) {
|
||||||
|
if (wallet) walletId = wallet.id;
|
||||||
|
return done(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(done) {
|
||||||
|
self.storage.fetchAddress(opts.identifier, function(err, address) {
|
||||||
|
if (address) walletId = address.walletId;
|
||||||
|
return done(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(done) {
|
||||||
|
self.storage.fetchTxByHash(opts.identifier, function(err, tx) {
|
||||||
|
if (tx) walletId = tx.walletId;
|
||||||
|
return done(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (walletId) {
|
||||||
|
return self.storage.fetchWallet(walletId, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is identifier a txid form an incomming tx?
|
||||||
|
async.detectSeries(_.values(Constants.NETWORKS), function(network, nextNetwork) {
|
||||||
|
var bc = self._getBlockchainExplorer(network);
|
||||||
|
bc.getTransaction(opts.identifier, function(err, tx) {
|
||||||
|
if (err || !tx) return nextNetwork(err, false);
|
||||||
|
var outputs = _.first(self._normalizeTxHistory(tx)).outputs;
|
||||||
|
var toAddresses = _.pluck(outputs, 'address');
|
||||||
|
async.detect(toAddresses, function(addressStr, nextAddress) {
|
||||||
|
self.storage.fetchAddress(addressStr, function(err, address) {
|
||||||
|
if (err || !address) return nextAddress(err, false);
|
||||||
|
walletId = address.walletId;
|
||||||
|
nextAddress(null, true);
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
nextNetwork(err, !!walletId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
if (walletId) {
|
||||||
|
return self.storage.fetchWallet(walletId, cb);
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves wallet status.
|
* Retrieves wallet status.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
@ -950,19 +1021,17 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) {
|
||||||
|
|
||||||
|
|
||||||
WalletService.prototype._getBlockchainExplorer = function(network) {
|
WalletService.prototype._getBlockchainExplorer = function(network) {
|
||||||
if (!this.blockchainExplorer) {
|
var opts = {};
|
||||||
var opts = {};
|
|
||||||
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) {
|
|
||||||
opts = this.blockchainExplorerOpts[network];
|
|
||||||
}
|
|
||||||
// TODO: provider should be configurable
|
|
||||||
opts.provider = 'insight';
|
|
||||||
opts.network = network;
|
|
||||||
opts.userAgent = WalletService.getServiceVersion();
|
|
||||||
this.blockchainExplorer = new BlockchainExplorer(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.blockchainExplorer;
|
if (this.blockchainExplorer) return this.blockchainExplorer;
|
||||||
|
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) {
|
||||||
|
opts = this.blockchainExplorerOpts[network];
|
||||||
|
}
|
||||||
|
// TODO: provider should be configurable
|
||||||
|
opts.provider = 'insight';
|
||||||
|
opts.network = network;
|
||||||
|
opts.userAgent = WalletService.getServiceVersion();
|
||||||
|
return new BlockchainExplorer(opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._getUtxos = function(addresses, cb) {
|
WalletService.prototype._getUtxos = function(addresses, cb) {
|
||||||
|
@ -2566,7 +2635,7 @@ WalletService.prototype.getNotifications = function(opts, cb) {
|
||||||
WalletService.prototype._normalizeTxHistory = function(txs) {
|
WalletService.prototype._normalizeTxHistory = function(txs) {
|
||||||
var now = Math.floor(Date.now() / 1000);
|
var now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
return _.map(txs, function(tx) {
|
return _.map([].concat(txs), function(tx) {
|
||||||
var inputs = _.map(tx.vin, function(item) {
|
var inputs = _.map(tx.vin, function(item) {
|
||||||
return {
|
return {
|
||||||
address: item.addr,
|
address: item.addr,
|
||||||
|
|
|
@ -103,6 +103,7 @@ describe('Wallet service', function() {
|
||||||
message: 'hello world',
|
message: 'hello world',
|
||||||
signature: sig,
|
signature: sig,
|
||||||
clientVersion: 'bwc-2.0.0',
|
clientVersion: 'bwc-2.0.0',
|
||||||
|
walletId: '123',
|
||||||
}, function(err, server) {
|
}, function(err, server) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
server.walletId.should.equal(wallet.id);
|
server.walletId.should.equal(wallet.id);
|
||||||
|
@ -140,6 +141,36 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get server instance for support staff', function(done) {
|
||||||
|
helpers.createAndJoinWallet(1, 1, function(s, wallet) {
|
||||||
|
var collections = require('../../lib/storage').collections;
|
||||||
|
s.storage.db.collection(collections.COPAYERS_LOOKUP).update({
|
||||||
|
copayerId: wallet.copayers[0].id
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
isSupportStaff: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var xpriv = TestData.copayers[0].xPrivKey;
|
||||||
|
var priv = TestData.copayers[0].privKey_1H_0;
|
||||||
|
|
||||||
|
var sig = helpers.signMessage('hello world', priv);
|
||||||
|
|
||||||
|
WalletService.getInstanceWithAuth({
|
||||||
|
copayerId: wallet.copayers[0].id,
|
||||||
|
message: 'hello world',
|
||||||
|
signature: sig,
|
||||||
|
walletId: '123',
|
||||||
|
}, function(err, server) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.walletId.should.equal('123');
|
||||||
|
server.copayerId.should.equal(wallet.copayers[0].id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Session management (#login, #logout, #authenticate)', function() {
|
describe('Session management (#login, #logout, #authenticate)', function() {
|
||||||
|
@ -4364,7 +4395,6 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('#getSendMaxInfo', function() {
|
describe('#getSendMaxInfo', function() {
|
||||||
var server, wallet;
|
var server, wallet;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
|
@ -5209,7 +5239,6 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Tx proposal workflow', function() {
|
describe('Tx proposal workflow', function() {
|
||||||
|
@ -7446,4 +7475,107 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getWalletFromIdentifier', function() {
|
||||||
|
var server, wallet;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(1, 1, {}, function(s, w) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get wallet from id', function(done) {
|
||||||
|
server.getWalletFromIdentifier({
|
||||||
|
identifier: wallet.id
|
||||||
|
}, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(w);
|
||||||
|
w.id.should.equal(wallet.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should get wallet from address', function(done) {
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(address);
|
||||||
|
server.getWalletFromIdentifier({
|
||||||
|
identifier: address.address
|
||||||
|
}, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(w);
|
||||||
|
w.id.should.equal(wallet.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should get wallet from tx proposal', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, '1 btc', function() {
|
||||||
|
helpers.stubBroadcast();
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1000e2
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
message: 'some message',
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(txp) {
|
||||||
|
should.exist(txp);
|
||||||
|
var signatures = helpers.clientSign(txp, TestData.copayers[0].xPrivKey_44H_0H_0H);
|
||||||
|
server.signTx({
|
||||||
|
txProposalId: txp.id,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.getPendingTxs({}, function(err, txps) {
|
||||||
|
should.not.exist(err);
|
||||||
|
txp = txps[0];
|
||||||
|
server.getWalletFromIdentifier({
|
||||||
|
identifier: txp.txid
|
||||||
|
}, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(w);
|
||||||
|
w.id.should.equal(wallet.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should get wallet from incoming txid', function(done) {
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(address);
|
||||||
|
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, {
|
||||||
|
txid: '999',
|
||||||
|
vout: [{
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: [address.address]
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
server.getWalletFromIdentifier({
|
||||||
|
identifier: '999'
|
||||||
|
}, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(w);
|
||||||
|
w.id.should.equal(wallet.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return nothing if identifier not associated with a wallet', function(done) {
|
||||||
|
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null);
|
||||||
|
server.getWalletFromIdentifier({
|
||||||
|
identifier: 'dummy'
|
||||||
|
}, function(err, w) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.not.exist(w);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue