You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

668 lines
17 KiB

'use strict';
var _ = require('lodash');
var $ = require('preconditions').singleton();
var util = require('util');
var async = require('async');
var log = require('npmlog');
var request = require('request')
10 years ago
var events = require('events');
log.debug = log.verbose;
var Bitcore = require('bitcore')
10 years ago
var Credentials = require('./credentials');
var WalletUtils = require('../walletutils');
10 years ago
var Verifier = require('./verifier');
var ServerCompromisedError = require('./servercompromisederror');
var ClientError = require('../clienterror');
10 years ago
var BASE_URL = 'http://localhost:3001/copay/api';
function _encryptMessage(message, encryptingKey) {
if (!message) return null;
return WalletUtils.encryptMessage(message, encryptingKey);
};
function _decryptMessage(message, encryptingKey) {
if (!message) return '';
try {
return WalletUtils.decryptMessage(message, encryptingKey);
} catch (ex) {
return '<ECANNOTDECRYPT>';
}
10 years ago
};
10 years ago
function _processTxps(txps, encryptingKey) {
10 years ago
if (!txps) return;
_.each([].concat(txps), function(txp) {
txp.encryptedMessage = txp.message;
txp.message = _decryptMessage(txp.message, encryptingKey);
_.each(txp.actions, function(action) {
action.comment = _decryptMessage(action.comment, encryptingKey);
});
});
};
10 years ago
function _parseError(body) {
if (_.isString(body)) {
10 years ago
try {
body = JSON.parse(body);
10 years ago
} catch (e) {
body = {
error: body
};
10 years ago
}
10 years ago
}
var ret;
if (body.code) {
ret = new ClientError(body.code, body.message);
} else {
ret = {
code: 'ERROR',
error: body.error || 'There was an unknown error processing the request',
};
}
log.error(ret);
return ret;
10 years ago
};
function _signRequest(method, url, args, privKey) {
var message = method.toLowerCase() + '|' + url + '|' + JSON.stringify(args);
return WalletUtils.signMessage(message, privKey);
10 years ago
};
function API(opts) {
10 years ago
opts = opts || {};
this.verbose = !!opts.verbose;
10 years ago
this.request = opts.request || request;
10 years ago
this.baseUrl = opts.baseUrl || BASE_URL;
this.basePath = this.baseUrl.replace(/http.?:\/\/[a-zA-Z0-9:-]*\//, '/');
if (this.verbose) {
log.level = 'debug';
10 years ago
} else {
log.level = 'info';
10 years ago
}
};
10 years ago
util.inherits(API, events.EventEmitter);
10 years ago
10 years ago
API.prototype.seedFromAirGapped = function(seed) {
this.credentials = Credentials.fromAirGapped(seed.network, seed.xPubKey, seed.requestPrivKey);
10 years ago
};
10 years ago
API.prototype._doRequest = function(method, url, args, cb) {
$.checkState(this.credentials);
10 years ago
var reqSignature;
10 years ago
if (this.credentials.requestPrivKey) {
reqSignature = _signRequest(method, url, args, this.credentials.requestPrivKey);
10 years ago
}
10 years ago
var absUrl = this.baseUrl + url;
var args = {
10 years ago
// relUrl: only for testing with `supertest`
relUrl: this.basePath + url,
10 years ago
headers: {
10 years ago
'x-identity': this.credentials.copayerId,
10 years ago
'x-signature': reqSignature,
},
method: method,
10 years ago
url: absUrl,
body: args,
json: true,
};
10 years ago
10 years ago
log.verbose('Request Args', util.inspect(args, {
depth: 10
}));
this.request(args, function(err, res, body) {
10 years ago
log.verbose(util.inspect(body, {
depth: 10
}));
10 years ago
if (err) return cb(err);
10 years ago
if (res.statusCode != 200) {
return cb(_parseError(body));
10 years ago
}
return cb(null, body, res.header);
10 years ago
});
};
10 years ago
API.prototype._doPostRequest = function(url, args, cb) {
return this._doRequest('post', url, args, cb);
10 years ago
};
10 years ago
API.prototype._doGetRequest = function(url, cb) {
return this._doRequest('get', url, {}, cb);
10 years ago
};
API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) {
var args = {
walletId: walletId,
name: copayerName,
xPubKey: xPubKey,
xPubKeySignature: WalletUtils.signMessage(xPubKey, walletPrivKey),
};
var url = '/v1/wallets/' + walletId + '/copayers';
10 years ago
this._doPostRequest(url, args, function(err, body) {
if (err) return cb(err);
return cb(null, body.wallet);
});
};
10 years ago
API.prototype.isComplete = function() {
return this.credentials && this.credentials.isComplete();
};
10 years ago
10 years ago
/**
* Opens a wallet and tries to complete the public key ring.
* @param {Function} cb - Returns an error and a flag indicating that the wallet has just been completed and needs to be persisted
*/
API.prototype.openWallet = function(cb) {
$.checkState(this.credentials);
10 years ago
10 years ago
var self = this;
10 years ago
if (self.credentials.isComplete()) return cb(null, false);
10 years ago
10 years ago
var url = '/v1/wallets/';
self._doGetRequest(url, function(err, ret) {
if (err) return cb(err);
var wallet = ret.wallet;
10 years ago
10 years ago
if (wallet.status != 'complete')
return cb('Wallet Incomplete');
if (!Verifier.checkCopayers(self.credentials, wallet.copayers)) {
return cb(new ServerCompromisedError(
'Copayers in the wallet could not be verified to have known the wallet secret'));
}
self.credentials.addPublicKeyRing(_.pluck(wallet.copayers, 'xPubKey'));
return cb(null, true);
10 years ago
});
};
API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb) {
10 years ago
var self = this;
10 years ago
10 years ago
network = network || 'livenet';
if (!_.contains(['testnet', 'livenet'], network))
return cb('Invalid network');
10 years ago
10 years ago
if (!self.credentials) {
self.credentials = Credentials.create(network);
}
10 years ago
$.checkState(network == self.credentials.network);
10 years ago
var walletPrivKey = new Bitcore.PrivateKey();
var args = {
name: walletName,
m: m,
n: n,
pubKey: walletPrivKey.toPublicKey().toString(),
network: network,
};
var url = '/v1/wallets/';
self._doPostRequest(url, args, function(err, body) {
if (err) return cb(err);
10 years ago
var walletId = body.walletId;
10 years ago
var secret = WalletUtils.toSecret(walletId, walletPrivKey, network);
self.credentials.addWalletInfo(walletId, walletName, m, n, walletPrivKey.toString(), copayerName);
self._doJoinWallet(walletId, walletPrivKey, self.credentials.xPubKey, copayerName,
function(err, wallet) {
if (err) return cb(err);
return cb(null, n > 1 ? secret : null);
});
});
};
10 years ago
10 years ago
// API.prototype.reCreateWallet = function(walletName, cb) {
// var self = this;
// this._loadAndCheck(function(err, wcd) {
// if (err) return cb(err);
10 years ago
10 years ago
// var walletPrivKey = new Bitcore.PrivateKey();
// var args = {
// name: walletName,
// m: wcd.m,
// n: wcd.n,
// pubKey: walletPrivKey.toPublicKey().toString(),
// network: wcd.network,
// };
// var url = '/v1/wallets/';
// self._doPostRequest(url, args, function(err, body) {
// if (err) return cb(err);
10 years ago
10 years ago
// var walletId = body.walletId;
10 years ago
10 years ago
// var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network);
// var i = 0;
// async.each(wcd.publicKeyRing, function(xpub, next) {
// var copayerName = 'recovered Copayer #' + i;
// self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next);
// }, function(err) {
// return cb(err);
// });
// });
// });
// };
10 years ago
API.prototype.joinWallet = function(secret, copayerName, cb) {
10 years ago
var self = this;
10 years ago
10 years ago
try {
var secretData = WalletUtils.fromSecret(secret);
} catch (ex) {
return cb(ex);
}
10 years ago
10 years ago
if (!self.credentials) {
self.credentials = Credentials.create(secretData.network);
}
10 years ago
self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, self.credentials.xPubKey, copayerName,
function(err, wallet) {
if (err) return cb(err);
self.credentials.addWalletInfo(wallet.id, wallet.name, wallet.m, wallet.n, secretData.walletPrivKey, copayerName);
return cb(null, wallet);
});
};
API.prototype.getStatus = function(cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
10 years ago
var self = this;
10 years ago
var url = '/v1/wallets/';
self._doGetRequest(url, function(err, result) {
_processTxps(result.pendingTxps, self.credentials.sharedEncryptingKey);
return cb(err, result, self.credentials.copayerId);
});
};
10 years ago
/**
* send
*
* @param opts
* @param opts.toAddress
* @param opts.amount
* @param opts.message
10 years ago
*/
API.prototype.sendTxProposal = function(opts, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(opts);
$.shouldBeNumber(opts.amount);
10 years ago
var self = this;
10 years ago
var args = {
toAddress: opts.toAddress,
amount: opts.amount,
message: _encryptMessage(opts.message, self.credentials.sharedEncryptingKey),
};
var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message);
args.proposalSignature = WalletUtils.signMessage(hash, self.credentials.requestPrivKey);
log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature);
10 years ago
var url = '/v1/txproposals/';
self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
10 years ago
});
};
API.prototype.createAddress = function(cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
10 years ago
var url = '/v1/addresses/';
self._doPostRequest(url, {}, function(err, address) {
10 years ago
if (err) return cb(err);
10 years ago
if (!Verifier.checkAddress(self.credentials, address)) {
return cb(new ServerCompromisedError('Server sent fake address'));
}
10 years ago
10 years ago
return cb(null, address);
10 years ago
});
};
/*
* opts.doNotVerify
*/
API.prototype.getMainAddresses = function(opts, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
10 years ago
var url = '/v1/addresses/';
self._doGetRequest(url, function(err, addresses) {
if (err) return cb(err);
10 years ago
if (!opts.doNotVerify) {
var fake = _.any(addresses, function(address) {
return !Verifier.checkAddress(self.credentials, address);
});
if (fake)
return cb(new ServerCompromisedError('Server sent fake address'));
}
return cb(null, addresses);
});
};
API.prototype.getBalance = function(cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
10 years ago
var url = '/v1/balance/';
self._doGetRequest(url, cb);
};
/**
* Export does not try to complete the wallet from the server. Exports the
* wallet as it is now.
*
* @param opts.access =['full', 'readonly', 'readwrite']
*/
10 years ago
// API.prototype.export = function(opts, cb) {
// $.checkState(this.credentials);
// $.shouldBeFunction(cb);
10 years ago
// var self = this;
10 years ago
10 years ago
// opts = opts || {};
// var access = opts.access || 'full';
10 years ago
10 years ago
// this._load(function(err, wcd) {
// if (err) return cb(err);
// var v = [];
10 years ago
// var myXPubKey = wcd.xPrivKey ? (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString() : '';
10 years ago
// _.each(WALLET_CRITICAL_DATA, function(k) {
// var d;
10 years ago
// if (access != 'full' && k === 'xPrivKey') {
// v.push(null);
// return;
// }
10 years ago
// // Skips own pub key IF priv key is exported
// if (access == 'full' && k === 'publicKeyRing') {
// d = _.without(wcd[k], myXPubKey);
// } else {
// d = wcd[k];
// }
// v.push(d);
// });
10 years ago
// if (access != 'full') {
// v.push(wcd.copayerId);
// v.push(wcd.roPrivKey);
// if (access == 'readwrite') {
// v.push(wcd.requestPrivKey);
// }
// }
10 years ago
// return cb(null, JSON.stringify(v));
// });
// }
10 years ago
// API.prototype.import = function(str, cb) {
// var self = this;
10 years ago
// this.storage.load(function(err, wcd) {
// if (wcd)
// return cb('Storage already contains a wallet');
10 years ago
10 years ago
// wcd = {};
10 years ago
10 years ago
// var inData = JSON.parse(str);
// var i = 0;
10 years ago
10 years ago
// _.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) {
// wcd[k] = inData[i++];
// });
10 years ago
// if (wcd.xPrivKey) {
// var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey);
// var xPubKey = new Bitcore.HDPublicKey(xpriv).toString();
// wcd.publicKeyRing.unshift(xPubKey);
// wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey);
// wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF();
// wcd.requestPrivKey = xpriv.derive('m/1/1').privateKey.toWIF();
// }
10 years ago
10 years ago
// if (!wcd.publicKeyRing)
// return cb('Invalid source wallet');
// wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet';
// self.save(wcd, function(err) {
// return cb(err, WalletUtils.accessFromData(wcd));
// });
// });
// };
10 years ago
/**
*
*/
API.prototype.parseTxProposals = function(txData, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
10 years ago
10 years ago
var self = this;
10 years ago
10 years ago
var txps = txData.txps;
_processTxps(txps, self.credentials.sharedEncryptingKey);
10 years ago
10 years ago
var fake = _.any(txps, function(txp) {
return (!Verifier.checkTxProposal(self.credentials, txp));
});
10 years ago
10 years ago
if (fake)
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
10 years ago
10 years ago
return cb(null, txps);
10 years ago
};
10 years ago
/**
*
10 years ago
* opts.doNotVerify
10 years ago
* opts.getRawTxps
10 years ago
* @return {undefined}
*/
API.prototype.getTxProposals = function(opts, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
10 years ago
var url = '/v1/txproposals/';
self._doGetRequest(url, function(err, txps) {
if (err) return cb(err);
10 years ago
var rawTxps;
if (opts.getRawTxps) {
rawTxps = JSON.parse(JSON.stringify(txps));
}
10 years ago
10 years ago
_processTxps(txps, self.credentials.sharedEncryptingKey);
10 years ago
var fake = _.any(txps, function(txp) {
return (!opts.doNotVerify && !Verifier.checkTxProposal(self.credentials, txp));
});
10 years ago
if (fake)
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
10 years ago
// TODO: return a single arg
return cb(null, txps, rawTxps);
});
};
10 years ago
API.prototype._getSignaturesFor = function(txp) {
var self = this;
10 years ago
//Derive proper key to sign, for each input
var privs = [],
derived = {};
var network = new Bitcore.Address(txp.toAddress).network.name;
10 years ago
var xpriv = new Bitcore.HDPrivateKey(self.credentials.xPrivKey, network);
10 years ago
_.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.address);
10 years ago
10 years ago
var signatures = _.map(privs, function(priv, i) {
return t.getSignatures(priv);
});
signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) {
return s.signature.toDER().toString('hex');
});
return signatures;
};
API.prototype.getSignatures = function(txp, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
10 years ago
$.checkArgument(txp.creatorId);
10 years ago
var self = this;
10 years ago
if (!self.credentials.canSign())
return cb('You do not have the required keys to sign transactions');
10 years ago
10 years ago
if (!Verifier.checkTxProposal(self.credentials, txp)) {
return cb(new ServerCompromisedError('Transaction proposal is invalid'));
}
10 years ago
return cb(null, self._getSignaturesFor(txp));
10 years ago
};
10 years ago
10 years ago
// API.prototype.getEncryptedWalletData = function(cb) {
// var self = this;
10 years ago
// this._loadAndCheck(function(err, wcd) {
// if (err) return cb(err);
// var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE));
// return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey)));
// });
// };
10 years ago
API.prototype.signTxProposal = function(txp, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
10 years ago
$.checkArgument(txp.creatorId);
10 years ago
10 years ago
var self = this;
10 years ago
10 years ago
if (!self.credentials.canSign() && !txp.signatures)
return cb(new Error('You do not have the required keys to sign transactions'));
10 years ago
10 years ago
if (!Verifier.checkTxProposal(self.credentials, txp)) {
return cb(new ServerCompromisedError('Server sent fake transaction proposal'));
}
10 years ago
10 years ago
var signatures = txp.signatures || self._getSignaturesFor(txp);
10 years ago
10 years ago
var url = '/v1/txproposals/' + txp.id + '/signatures/';
var args = {
signatures: signatures
};
10 years ago
10 years ago
self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
10 years ago
};
API.prototype.rejectTxProposal = function(txp, reason, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
$.checkArgument(cb);
10 years ago
var self = this;
10 years ago
var url = '/v1/txproposals/' + txp.id + '/rejections/';
var args = {
reason: _encryptMessage(reason, self.credentials.sharedEncryptingKey) || '',
};
self._doPostRequest(url, args, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
10 years ago
};
10 years ago
API.prototype.broadcastTxProposal = function(txp, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
10 years ago
10 years ago
var self = this;
10 years ago
10 years ago
var url = '/v1/txproposals/' + txp.id + '/broadcast/';
self._doPostRequest(url, {}, function(err, txp) {
if (err) return cb(err);
return cb(null, txp);
});
10 years ago
};
10 years ago
API.prototype.removeTxProposal = function(txp, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
10 years ago
var self = this;
10 years ago
var url = '/v1/txproposals/' + txp.id;
self._doRequest('delete', url, {}, function(err) {
if (err) return cb(err);
return cb();
});
10 years ago
};
API.prototype.getTxHistory = function(opts, cb) {
10 years ago
$.checkState(this.credentials && this.credentials.isComplete());
var self = this;
10 years ago
var url = '/v1/txhistory/';
self._doGetRequest(url, function(err, txs) {
if (err) return cb(err);
10 years ago
_processTxps(txs, self.credentials.sharedEncryptingKey);
10 years ago
return cb(null, txs);
});
};
module.exports = API;