Merge pull request #10 from matiu/feat/cli-send2

Feat/cli send2
This commit is contained in:
Ivan Socolsky 2015-02-13 22:29:11 -03:00
commit b8532f662f
18 changed files with 274 additions and 95 deletions

21
app.js
View File

@ -39,6 +39,9 @@ app.use(bodyParser.json({
limit: POST_LIMIT
}));
app.use(require('morgan')('dev'));
var port = process.env.COPAY_PORT || 3001;
var router = express.Router();
@ -59,7 +62,6 @@ function returnError(err, res, req) {
}
var m = message || err.toString();
console.log('[app.js.60]'); //TODO
log.error('Error: ' + req.url + ' :' + code + ':' + m);
res.status(code || 500).json({
error: m,
@ -144,6 +146,17 @@ router.get('/v1/wallets/', function(req, res) {
});
});
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) {
@ -181,9 +194,9 @@ router.get('/v1/balance/', function(req, res) {
});
});
router.post('/v1/txproposals/:id/signatures', function(req, res) {
req.body.txProposalId = req.params['id'];
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.end();
@ -192,8 +205,8 @@ router.post('/v1/txproposals/:id/signatures', function(req, res) {
});
router.post('/v1/txproposals/:id/rejections', function(req, res) {
req.body.txProposalId = req.params['id'];
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.end();

View File

@ -1,7 +1,6 @@
#!/usr/bin/env node
var program = require('commander');
var cli = require('../lib/clilib.js');
program
.version('0.0.1')
@ -12,6 +11,7 @@ program
.command('addresses', 'list addresses')
.command('balance', 'wallet balance')
.command('send <address> <amount> <note>', 'send bitcoins')
.command('sign <txpId>', 'sign a Transaction Proposal')
.parse(process.argv);

4
bit-wallet/bit-address Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
@ -11,7 +11,7 @@ program
.parse(process.argv);
var args = program.args;
var cli = new CliLib({
var cli = new ClientLib({
filename: program.config
});

4
bit-wallet/bit-addresses Normal file → Executable file
View File

@ -2,8 +2,8 @@
var _ = require('lodash');
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var common = require('./common');
var ClientLib = require('../lib/clientlib.js');
program
.version('0.0.1')
@ -12,7 +12,7 @@ program
.parse(process.argv);
var args = program.args;
var cli = new CliLib({
var cli = new ClientLib({
filename: program.config
});

4
bit-wallet/bit-balance Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
@ -11,7 +11,7 @@ program
.parse(process.argv);
var args = program.args;
var cli = new CliLib({
var cli = new ClientLib({
filename: program.config
});

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
@ -21,7 +21,7 @@ var network = program.network;
var mn = common.parseMN(args[1]);
var cli = new CliLib({
var cli = new ClientLib({
filename: program.config
});
cli.createWallet(walletName, copayerName, mn[0], mn[1], network, function(err, secret) {

4
bit-wallet/bit-join Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
@ -17,7 +17,7 @@ if (!args[0])
var secret = args[0];
var copayerName = args[1] || process.env.USER;
var cli = new CliLib({
var cli = new ClientLib({
filename: program.config
});

12
bit-wallet/bit-send Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
@ -19,15 +19,15 @@ if (!args[0] || !args[1] || !args[2])
var amount = args[1];
var message = args[2];
var cli = new CliLib({
filename: program.config
var cli = new ClientLib({
filename: program.config
});
cli.send({toAddress: address, amount: amount, message:message}, function(err, x) {
common.die(err);
console.log(' * Tx created: ID %s [%s] RequiredSignatures:',
x.id, x.status, x.requiredSignatures);
x.id, x.status, x.requiredSignatures);
if (program.verbose)
console.log('* Raw Server Response:\n', x); //TODO
});
console.log('* Raw Server Response:\n', x); //TODO
});

52
bit-wallet/bit-sign Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env node
var _ = require('lodash');
var program = require('commander');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
.version('0.0.1')
.option('-c,--config [file]', 'Wallet config filename')
.option('-v,--verbose', 'be verbose')
.usage('[options] <txpid>')
.parse(process.argv);
var args = program.args;
if (!args[0])
program.help();
var txpid = args[0];
var cli = new ClientLib({
filename: program.config
});
cli.txProposals({}, function(err, x) {
common.die(err);
if (program.verbose)
console.log('* Raw Server Response:\n', x); //TODO
var txps = _.filter(x, function(x) {
return _.endsWith(common.shortID(x.id), txpid);
});
if (!txps.length)
common.die('Could not find TX Proposal:' + txpid);
if (txps.length > 1)
common.die('More that one TX Proposals match:' + txpid + ' : ' + _.map(txps, function(x) {
return x.id;
}).join(' '));;
var txp = txps[0];
cli.sign(txp, function(err, x) {
common.die(err);
if (program.verbose)
console.log('* Raw Server Response:\n', x); //TODO
console.log('Transaction signed.');
});
});

31
bit-wallet/bit-status Normal file → Executable file
View File

@ -1,7 +1,9 @@
#!/usr/bin/env node
var _ = require('lodash');
var program = require('commander');
var CliLib = require('../lib/clilib.js');
var ClientLib = require('../lib/clientlib.js');
var common = require('./common');
program
@ -11,14 +13,33 @@ program
.parse(process.argv);
var args = program.args;
var cli = new CliLib({
var cli = new ClientLib({
filename: program.config
});
cli.status(function(err, x) {
cli.status(function(err, res) {
common.die(err);
var x = res.wallet;
console.log('* Wallet %s [%s]: %d-%d %s ', x.name, x.isTestnet ? 'testnet' : 'livenet', x.m, x.n, x.status);
if (program.verbose)
console.log('* Raw Server Response:\n', x); //TODO
var x = res.balance;
console.log('* Balance %d (Locked: %d)', x.totalAmount, x.lockedAmount);
if (!_.isEmpty(res.pendingTxps)) {
console.log("* TX Proposals:")
_.each(res.pendingTxps, function(x) {
console.log("\t%s [%s by %s] %dSAT => %s", common.shortID(x.id), x.message, x.creatorName, x.amount, x.toAddress);
if (!_.isEmpty(x.actions)) {
console.log('\t\t * Actions');
console.log('\t\t', _.map(x.actions, function(a) {
return a.copayerName + ': ' + a.type + ''
}).join('. '));
}
if (program.verbose)
console.log('* Raw Server Response:\n', res); //TODO
});
}
});

View File

@ -24,4 +24,8 @@ common.parseMN = function(MN) {
};
common.shortID = function(id) {
return id.substr(id.length - 4);
};
module.exports = common;

View File

@ -11,7 +11,7 @@ var fs = require('fs')
var Bitcore = require('bitcore')
var SignUtils = require('./signutils');
var BASE_URL = 'http://localhost:3001/copay/api/';
var BASE_URL = 'http://localhost:3001/copay/api';
function _createProposalOpts(opts, signingKey) {
var msg = opts.toAddress + '|' + opts.amount + '|' + opts.message;
@ -43,29 +43,28 @@ function _signRequest(url, args, privKey) {
return SignUtils.sign(message, privKey);
};
function _createXPrivKey() {
return new Bitcore.HDPrivateKey().toString();
function _createXPrivKey(network) {
return new Bitcore.HDPrivateKey(network).toString();
};
function CliLib(opts) {
function ClientLib(opts) {
if (!opts.filename) {
throw new Error('Please set the config filename');
}
this.filename = opts.filename;
};
CliLib.prototype._save = function(data) {
ClientLib.prototype._save = function(data) {
fs.writeFileSync(this.filename, JSON.stringify(data));
};
CliLib.prototype._load = function() {
ClientLib.prototype._load = function() {
try {
return JSON.parse(fs.readFileSync(this.filename));
} catch (ex) {}
};
CliLib.prototype._loadAndCheck = function() {
ClientLib.prototype._loadAndCheck = function() {
var data = this._load();
if (!data) {
log.error('Wallet file not found.');
@ -85,18 +84,18 @@ CliLib.prototype._loadAndCheck = function() {
return data;
};
CliLib.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) {
ClientLib.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) {
var self = this;
network = network || 'livenet';
var data = this._load();
if (data) return cb('File ' + this.filename + ' already contains a wallet');
// Generate wallet key pair to verify copayers
var privKey = new Bitcore.PrivateKey();
var privKey = new Bitcore.PrivateKey(null, network);
var pubKey = privKey.toPublicKey();
data = {
xPrivKey: _createXPrivKey(),
m: m,
n: n,
walletPrivKey: privKey.toString(),
@ -107,7 +106,7 @@ CliLib.prototype.createWallet = function(walletName, copayerName, m, n, network,
m: m,
n: n,
pubKey: pubKey.toString(),
network: network || 'livenet',
network: network,
};
request({
@ -123,11 +122,10 @@ CliLib.prototype.createWallet = function(walletName, copayerName, m, n, network,
}
var walletId = body.walletId;
var secret = walletId + ':' + privKey.toString();
var secret = walletId + ':' + privKey.toString() + ':' + (network ? 'T' : 'L');
data.secret = secret;
self._save(data);
self._joinWallet(data, secret, copayerName, function(err) {
if (err) return cb(err);
@ -136,18 +134,21 @@ CliLib.prototype.createWallet = function(walletName, copayerName, m, n, network,
});
};
CliLib.prototype._joinWallet = function(data, secret, copayerName, cb) {
ClientLib.prototype._joinWallet = function(data, secret, copayerName, cb) {
var self = this;
data = data || {};
var secretSplit = secret.split(':');
var walletId = secretSplit[0];
var walletPrivKey = Bitcore.PrivateKey.fromString(secretSplit[1]);
var network = secretSplit[2] == 'T' ? 'testnet' : 'livenet';
data.xPrivKey = _createXPrivKey(network);
var xPubKey = new Bitcore.HDPublicKey(data.xPrivKey);
var xPubKeySignature = SignUtils.sign(xPubKey.toString(), walletPrivKey);
var signingPrivKey = (new Bitcore.HDPrivateKey(data.xPrivKey)).derive('m/1/0').privateKey;
var args = {
walletId: walletId,
name: copayerName,
@ -180,20 +181,16 @@ CliLib.prototype._joinWallet = function(data, secret, copayerName, cb) {
});
};
CliLib.prototype.joinWallet = function(secret, copayerName, cb) {
ClientLib.prototype.joinWallet = function(secret, copayerName, cb) {
var self = this;
var data = this._load();
if (data) return cb('File ' + this.filename + ' already contains a wallet');
data = {
xPrivKey: _createXPrivKey(),
};
self._joinWallet(data, secret, copayerName, cb);
};
CliLib.prototype.status = function(cb) {
ClientLib.prototype.status = function(cb) {
var self = this;
var data = this._loadAndCheck();
@ -253,7 +250,7 @@ CliLib.prototype.status = function(cb) {
* @param inArgs.amount
* @param inArgs.message
*/
CliLib.prototype.send = function(inArgs, cb) {
ClientLib.prototype.send = function(inArgs, cb) {
var self = this;
var data = this._loadAndCheck();
@ -282,16 +279,16 @@ CliLib.prototype.send = function(inArgs, cb) {
};
// TODO check change address
CliLib.prototype.sign = function(proposalId, cb) {
ClientLib.prototype.sign = function(proposalId, cb) {
};
CliLib.prototype.reject = function(proposalId, cb) {
ClientLib.prototype.reject = function(proposalId, cb) {
};
// Get addresses
CliLib.prototype.addresses = function(cb) {
ClientLib.prototype.addresses = function(cb) {
var self = this;
var data = this._loadAndCheck();
@ -320,7 +317,7 @@ CliLib.prototype.addresses = function(cb) {
// Creates a new address
// TODO: verify derivation!!
CliLib.prototype.address = function(cb) {
ClientLib.prototype.address = function(cb) {
var self = this;
var data = this._loadAndCheck();
@ -346,11 +343,11 @@ CliLib.prototype.address = function(cb) {
});
};
CliLib.prototype.history = function(limit, cb) {
ClientLib.prototype.history = function(limit, cb) {
};
CliLib.prototype.balance = function(cb) {
ClientLib.prototype.balance = function(cb) {
var self = this;
var data = this._loadAndCheck();
@ -377,7 +374,7 @@ CliLib.prototype.balance = function(cb) {
};
CliLib.prototype.txProposals = function(cb) {
ClientLib.prototype.txProposals = function(opts, cb) {
var self = this;
var data = this._loadAndCheck();
@ -403,5 +400,65 @@ CliLib.prototype.txProposals = function(cb) {
});
};
ClientLib.prototype.sign = function(txp, cb) {
var self = this;
var data = this._loadAndCheck();
module.exports = CliLib;
//Derive proper key to sign, for each input
var privs = [],
derived = {};
var network = new Bitcore.Address(txp.toAddress).network.name;
var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey, network);
_.each(txp.inputs, function(i) {
if (!derived[i.path]) {
derived[i.path] = xpriv.derive(i.path).privateKey;
}
privs.push(derived[i.path]);
});
var t = new Bitcore.Transaction();
_.each(txp.inputs, function(i) {
t.from(i, i.publicKeys, txp.requiredSignatures);
});
t.to(txp.toAddress, txp.amount)
.change(txp.changeAddress)
.sign(privs);
var signatures = [];
_.each(privs, function(p) {
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
signatures.push(s);
});
var url = '/v1/txproposals/' + txp.id + '/signatures/';
var args = {
signatures: signatures
};
var reqSignature = _signRequest(url, args, data.signingPrivKey);
console.log('[clientlib.js.441:reqSignature:]',url, args, reqSignature); //TODO
request({
headers: {
'x-identity': data.copayerId,
'x-signature': reqSignature,
},
method: 'post',
url: _getUrl(url),
body: args,
json: true,
}, function(err, res, body) {
if (err) return cb(err);
if (res.statusCode != 200) {
_parseError(body);
return cb('Request error');
}
return cb(null, body);
});
};
module.exports = ClientLib;

View File

@ -69,6 +69,19 @@ TxProposal.prototype._updateStatus = function() {
};
TxProposal.prototype._getCurrentSignatures = function() {
var acceptedActions = _.filter(this.actions, function(x) {
return x && x.type == 'accept';
});
return _.map(acceptedActions, function(x) {
return {
signatures: x.signatures,
xpub: x.xpub,
};
});
};
TxProposal.prototype._getBitcoreTx = function() {
var self = this;
@ -81,6 +94,13 @@ TxProposal.prototype._getBitcoreTx = function() {
.change(this.changeAddress);
t._updateChangeOutput();
var sigs = this._getCurrentSignatures();
_.each(sigs, function(x) {
self._addSignaturesToBitcoreTx(t, x.signatures, x.xpub);
});
return t;
};
@ -121,22 +141,20 @@ TxProposal.prototype.getActionBy = function(copayerId) {
};
};
TxProposal.prototype.addAction = function(copayerId, type, signatures) {
TxProposal.prototype.addAction = function(copayerId, type, signatures, xpub) {
var action = new TxProposalAction({
copayerId: copayerId,
type: type,
signatures: signatures,
xpub: xpub,
});
this.actions[copayerId] = action;
this._updateStatus();
};
// TODO: no sure we should receive xpub or a list of pubkeys (pre derived)
TxProposal.prototype.checkSignatures = function(signatures, xpub) {
TxProposal.prototype._addSignaturesToBitcoreTx = function(t, signatures, xpub) {
var self = this;
var t = this._getBitcoreTx();
if (signatures.length != this.inputs.length)
return false;
@ -159,17 +177,25 @@ TxProposal.prototype.checkSignatures = function(signatures, xpub) {
t.applySignature(s);
oks++;
} catch (e) {
// TODO only for debug now
console.log('DEBUG ONLY:', e.message); //TODO
};
} catch (e) {};
});
return oks === t.inputs.length;
if (oks != t.inputs.length)
throw new Error('wrong signatures');
};
TxProposal.prototype.sign = function(copayerId, signatures) {
this.addAction(copayerId, 'accept', signatures);
TxProposal.prototype.sign = function(copayerId, signatures, xpub) {
// Tests signatures are OK
var t = this._getBitcoreTx();
try {
this._addSignaturesToBitcoreTx(t, signatures, xpub);
this.addAction(copayerId, 'accept', signatures, xpub);
return true;
} catch (e) {
return false;
}
};
TxProposal.prototype.reject = function(copayerId) {

View File

@ -7,6 +7,7 @@ function TxProposalAction(opts) {
this.copayerId = opts.copayerId;
this.type = opts.type || (opts.signatures ? 'accept' : 'reject');
this.signatures = opts.signatures;
this.xpub = opts.xpub;
};
TxProposalAction.fromObj = function (obj) {
@ -16,6 +17,7 @@ TxProposalAction.fromObj = function (obj) {
x.copayerId = obj.copayerId;
x.type = obj.type;
x.signatures = obj.signatures;
x.xpub = obj.xpub;
return x;
};

View File

@ -341,8 +341,11 @@ CopayServer.prototype._getUtxos = function(cb) {
var networkName = Bitcore.Address(addressStrs[0]).toObject().network;
var bc = self._getBlockExplorer('insight', networkName);
bc.getUnspentUtxos(addressStrs, function(err, utxos) {
bc.getUnspentUtxos(addressStrs, function(err, inutxos) {
if (err) return cb(err);
var utxos = _.map(inutxos, function(i) {
return i.toObject();
});
self.getPendingTxs({}, function(err, txps) {
if (err) return cb(err);
@ -581,6 +584,7 @@ CopayServer.prototype.removePendingTx = function(opts, cb) {
CopayServer.prototype._broadcastTx = function(txp, cb) {
var raw = txp.getRawTx();
console.log('[server.js.586:raw:]',raw); //TODO
var bc = this._getBlockExplorer('insight', txp.getNetworkName());
bc.broadcast(raw, function(err, txid) {
return cb(err, txid);
@ -617,11 +621,9 @@ CopayServer.prototype.signTx = function(opts, cb) {
var copayer = wallet.getCopayer(self.copayerId);
if (!txp.checkSignatures(opts.signatures, copayer.xPubKey))
if (!txp.sign(self.copayerId, opts.signatures, copayer.xPubKey))
return cb(new ClientError('BADSIGNATURES', 'Bad signatures'));
txp.sign(self.copayerId, opts.signatures);
self.storage.storeTx(self.walletId, txp, function(err) {
if (err) return cb(err);

View File

@ -18,7 +18,7 @@
},
"dependencies": {
"async": "^0.9.0",
"bitcore": "0.10.0",
"bitcore": "^0.10.3",
"bitcore-explorers": "^0.9.1",
"body-parser": "^1.11.0",
"commander": "^2.6.0",
@ -26,7 +26,8 @@
"inherits": "^2.0.1",
"leveldown": "^0.10.0",
"levelup": "^0.19.0",
"lodash": "^2.4.1",
"lodash": "^3.2.0",
"morgan": "*",
"npmlog": "^0.1.1",
"preconditions": "^1.0.7",
"request": "^2.53.0",

View File

@ -114,7 +114,14 @@ helpers.createUtxos = function(server, wallet, amounts, cb) {
};
helpers.stubBlockExplorer = function(server, utxos, txid) {
helpers.stubBlockExplorer = function(server, inUtxos, txid) {
var utxos = _.map(inUtxos, function(x) {
x.toObject = function() {
return this;
};
return x;
});
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);

View File

@ -28,15 +28,25 @@ describe('TXProposal', function() {
describe('#sign', function() {
it('should sign 2-2', function() {
var txp = TXP.fromObj(aTXP());
txp.sign('1', theSignatures);
txp.sign('1', theSignatures, theXPub);
txp.isAccepted().should.equal(false);
txp.isRejected().should.equal(false);
txp.sign('2', theSignatures);
txp.sign('2', theSignatures, theXPub);
txp.isAccepted().should.equal(true);
txp.isRejected().should.equal(false);
});
});
describe('#getRawTx', function() {
it('should generate correct raw transaction for signed 2-2', function() {
var txp = TXP.fromObj(aTXP());
txp.sign('1', theSignatures, theXPub);
txp.getRawTx().should.equal('0100000001ab069f7073be9b491bb1ad4233a45d2e383082ccc7206df905662d6d8499e66e080000009200483045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9014752210319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f2103b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d10130152aeffffffff0280f0fa02000000001976a91451224bca38efcaa31d5340917c3f3f713b8b20e488ac70c9fa020000000017a914778192003f0e9e1d865c082179cc3dae5464b03d8700000000');
});
});
describe('#reject', function() {
it('should reject 2-2', function() {
var txp = TXP.fromObj(aTXP());
@ -59,26 +69,10 @@ describe('TXProposal', function() {
});
});
describe('#checkSignatures', function() {
it('should check signatures', function() {
var txp = TXP.fromObj(aTXP());
var xpriv = new Bitcore.HDPrivateKey(theXPriv);
var priv = xpriv.derive(txp.inputPaths[0]).privateKey;
var t = txp._getBitcoreTx();
t.sign(priv);
var s = t.getSignatures(priv)[0].signature.toDER().toString('hex');
var xpub = new Bitcore.HDPublicKey(xpriv);
var res = txp.checkSignatures([s], xpub);
res.should.equal(true);
});
});
});
var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e';
var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9';
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
var aTXP = function() {