|
|
@ -11,11 +11,12 @@ log.debug = log.verbose; |
|
|
|
var Bitcore = require('bitcore') |
|
|
|
var WalletUtils = require('../walletutils'); |
|
|
|
var Verifier = require('./verifier'); |
|
|
|
var ServerCompromisedError = require('./servercompromisederror') |
|
|
|
var ServerCompromisedError = require('./servercompromisederror'); |
|
|
|
var ClientError = require('../clienterror'); |
|
|
|
|
|
|
|
var BASE_URL = 'http://localhost:3001/copay/api'; |
|
|
|
|
|
|
|
var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'publicKeyRing', 'sharedEncryptingKey']; |
|
|
|
var WALLET_CRITICAL_DATA = ['xPrivKey', 'm', 'n', 'publicKeyRing', 'sharedEncryptingKey']; |
|
|
|
var WALLET_EXTRA_DATA = ['copayerId', 'roPrivKey', 'rwPrivKey']; |
|
|
|
|
|
|
|
var WALLET_AIRGAPPED_TOCOMPLETE = ['publicKeyRing', 'm', 'n', 'sharedEncryptingKey']; |
|
|
@ -54,13 +55,17 @@ function _parseError(body) { |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
var code = body.code || 'ERROR'; |
|
|
|
var message = body.error || 'There was an unknown error processing the request'; |
|
|
|
log.error(code, message); |
|
|
|
return { |
|
|
|
message: message, |
|
|
|
code: code |
|
|
|
}; |
|
|
|
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; |
|
|
|
}; |
|
|
|
|
|
|
|
function _signRequest(method, url, args, privKey) { |
|
|
@ -68,6 +73,34 @@ function _signRequest(method, url, args, privKey) { |
|
|
|
return WalletUtils.signMessage(message, privKey); |
|
|
|
}; |
|
|
|
|
|
|
|
function _initWcd(network) { |
|
|
|
$.checkArgument(network); |
|
|
|
var xPrivKey = new Bitcore.HDPrivateKey(network); |
|
|
|
var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); |
|
|
|
var roPrivKey = xPrivKey.derive('m/1/0').privateKey; |
|
|
|
var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; |
|
|
|
var copayerId = WalletUtils.xPubToCopayerId(xPubKey); |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
copayerId: copayerId, |
|
|
|
xPrivKey: xPrivKey.toString(), |
|
|
|
publicKeyRing: [xPubKey], |
|
|
|
network: network, |
|
|
|
roPrivKey: roPrivKey.toWIF(), |
|
|
|
rwPrivKey: rwPrivKey.toWIF(), |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
function _addWalletToWcd(wcd, walletPrivKey, m, n) { |
|
|
|
$.checkArgument(wcd); |
|
|
|
var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); |
|
|
|
|
|
|
|
wcd.walletPrivKey = walletPrivKey.toWIF(); |
|
|
|
wcd.sharedEncryptingKey = sharedEncryptingKey; |
|
|
|
wcd.m = m; |
|
|
|
wcd.n = n; |
|
|
|
}; |
|
|
|
|
|
|
|
function API(opts) { |
|
|
|
if (!opts.storage) { |
|
|
@ -85,57 +118,60 @@ function API(opts) { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype._tryToCompleteFromServer = function(data, cb) { |
|
|
|
var self = this; |
|
|
|
API.prototype._tryToCompleteFromServer = function(wcd, cb) { |
|
|
|
|
|
|
|
if (!wcd.walletPrivKey) |
|
|
|
return cb('Could not perform that action. Wallet Incomplete'); |
|
|
|
|
|
|
|
var self = this; |
|
|
|
var url = '/v1/wallets/'; |
|
|
|
self._doGetRequest(url, data, function(err, ret) { |
|
|
|
self._doGetRequest(url, wcd, function(err, ret) { |
|
|
|
if (err) return cb(err); |
|
|
|
var wallet = ret.wallet; |
|
|
|
|
|
|
|
if (wallet.status != 'complete') |
|
|
|
return cb('Wallet Incomplete'); |
|
|
|
|
|
|
|
if (!Verifier.checkCopayers(wallet.copayers, data.walletPrivKey, |
|
|
|
data.xPrivKey, data.n)) { |
|
|
|
if (!Verifier.checkCopayers(wallet.copayers, wcd.walletPrivKey, |
|
|
|
wcd.xPrivKey, wcd.n)) { |
|
|
|
|
|
|
|
return cb(new ServerCompromisedError( |
|
|
|
'Copayers in the wallet could not be verified to have known the wallet secret')); |
|
|
|
} |
|
|
|
|
|
|
|
data.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') |
|
|
|
wcd.publicKeyRing = _.pluck(wallet.copayers, 'xPubKey') |
|
|
|
|
|
|
|
self.storage.save(data, function(err) { |
|
|
|
return cb(err, data); |
|
|
|
self.storage.save(wcd, function(err) { |
|
|
|
return cb(err, wcd); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
API.prototype._tryToCompleteFromData = function(data, toComplete, cb) { |
|
|
|
API.prototype._tryToCompleteFromData = function(wcd, toComplete, cb) { |
|
|
|
var inData = _decryptMessage(toComplete, |
|
|
|
WalletUtils.privateKeyToAESKey(data.roPrivKey)); |
|
|
|
WalletUtils.privateKeyToAESKey(wcd.roPrivKey)); |
|
|
|
if (!inData) |
|
|
|
return cb('Could not complete wallet'); |
|
|
|
|
|
|
|
try { |
|
|
|
inData = JSON.parse(inData); |
|
|
|
_.extend(data, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); |
|
|
|
_.extend(wcd, _.pick(inData, WALLET_AIRGAPPED_TOCOMPLETE)); |
|
|
|
} catch (ex) { |
|
|
|
return cb(ex); |
|
|
|
} |
|
|
|
|
|
|
|
this.storage.save(data, function(err) { |
|
|
|
return cb(err, data); |
|
|
|
this.storage.save(wcd, function(err) { |
|
|
|
return cb(err, wcd); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
API.prototype._tryToComplete = function(opts, data, cb) { |
|
|
|
API.prototype._tryToComplete = function(opts, wcd, cb) { |
|
|
|
if (opts.toComplete) { |
|
|
|
this._tryToCompleteFromData(data, opts.toComplete, cb); |
|
|
|
this._tryToCompleteFromData(wcd, opts.toComplete, cb); |
|
|
|
} else { |
|
|
|
this._tryToCompleteFromServer(data, cb); |
|
|
|
this._tryToCompleteFromServer(wcd, cb); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
@ -144,11 +180,11 @@ API.prototype._tryToComplete = function(opts, data, cb) { |
|
|
|
API.prototype._load = function(cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this.storage.load(function(err, data) { |
|
|
|
if (err || !data) { |
|
|
|
return cb(err || 'Wallet file not found.'); |
|
|
|
this.storage.load(function(err, wcd) { |
|
|
|
if (err || !wcd) { |
|
|
|
return cb(err || 'wcd file not found.'); |
|
|
|
} |
|
|
|
return cb(null, data); |
|
|
|
return cb(null, wcd); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -161,26 +197,27 @@ API.prototype._load = function(cb) { |
|
|
|
API.prototype._loadAndCheck = function(opts, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._load(function(err, data) { |
|
|
|
this._load(function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!data.n || (data.n > 1 && data.publicKeyRing.length != data.n)) |
|
|
|
return self._tryToComplete(opts, data, cb); |
|
|
|
if (!wcd.n || (wcd.n > 1 && wcd.publicKeyRing.length != wcd.n)) { |
|
|
|
return self._tryToComplete(opts, wcd, cb); |
|
|
|
} |
|
|
|
|
|
|
|
return cb(null, data); |
|
|
|
return cb(null, wcd); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype._doRequest = function(method, url, args, data, cb) { |
|
|
|
API.prototype._doRequest = function(method, url, args, wcd, cb) { |
|
|
|
var reqSignature; |
|
|
|
data = data || {}; |
|
|
|
wcd = wcd || {}; |
|
|
|
|
|
|
|
if (method == 'get') { |
|
|
|
if (data.roPrivKey) |
|
|
|
reqSignature = _signRequest(method, url, args, data.roPrivKey); |
|
|
|
if (wcd.roPrivKey) |
|
|
|
reqSignature = _signRequest(method, url, args, wcd.roPrivKey); |
|
|
|
} else { |
|
|
|
if (data.rwPrivKey) |
|
|
|
reqSignature = _signRequest(method, url, args, data.rwPrivKey); |
|
|
|
if (wcd.rwPrivKey) |
|
|
|
reqSignature = _signRequest(method, url, args, wcd.rwPrivKey); |
|
|
|
} |
|
|
|
|
|
|
|
var absUrl = this.baseUrl + url; |
|
|
@ -188,7 +225,7 @@ API.prototype._doRequest = function(method, url, args, data, cb) { |
|
|
|
// relUrl: only for testing with `supertest`
|
|
|
|
relUrl: this.basePath + url, |
|
|
|
headers: { |
|
|
|
'x-identity': data.copayerId, |
|
|
|
'x-identity': wcd.copayerId, |
|
|
|
'x-signature': reqSignature, |
|
|
|
}, |
|
|
|
method: method, |
|
|
@ -214,44 +251,15 @@ API.prototype._doRequest = function(method, url, args, data, cb) { |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
API.prototype._doPostRequest = function(url, args, data, cb) { |
|
|
|
return this._doRequest('post', url, args, data, cb); |
|
|
|
API.prototype._doPostRequest = function(url, args, wcd, cb) { |
|
|
|
return this._doRequest('post', url, args, wcd, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype._doGetRequest = function(url, data, cb) { |
|
|
|
return this._doRequest('get', url, {}, data, cb); |
|
|
|
API.prototype._doGetRequest = function(url, wcd, cb) { |
|
|
|
return this._doRequest('get', url, {}, wcd, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
API.prototype._initData = function(network, walletPrivKey, m, n) { |
|
|
|
var xPrivKey = new Bitcore.HDPrivateKey(network); |
|
|
|
var xPubKey = (new Bitcore.HDPublicKey(xPrivKey)).toString(); |
|
|
|
var roPrivKey = xPrivKey.derive('m/1/0').privateKey; |
|
|
|
var rwPrivKey = xPrivKey.derive('m/1/1').privateKey; |
|
|
|
var copayerId = WalletUtils.xPubToCopayerId(xPubKey); |
|
|
|
|
|
|
|
|
|
|
|
var data = { |
|
|
|
copayerId: copayerId, |
|
|
|
xPrivKey: xPrivKey.toString(), |
|
|
|
publicKeyRing: [xPubKey], |
|
|
|
network: network, |
|
|
|
roPrivKey: roPrivKey.toWIF(), |
|
|
|
rwPrivKey: rwPrivKey.toWIF(), |
|
|
|
}; |
|
|
|
|
|
|
|
if (walletPrivKey) { |
|
|
|
var sharedEncryptingKey = WalletUtils.privateKeyToAESKey(walletPrivKey); |
|
|
|
data.walletPrivKey = walletPrivKey.toWIF(); |
|
|
|
data.sharedEncryptingKey = sharedEncryptingKey; |
|
|
|
} |
|
|
|
|
|
|
|
if (m) data.m = m; |
|
|
|
if (n) data.n = n; |
|
|
|
|
|
|
|
return data; |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype._doJoinWallet = function(walletId, walletPrivKey, xPubKey, copayerName, cb) { |
|
|
|
var args = { |
|
|
|
walletId: walletId, |
|
|
@ -272,12 +280,12 @@ API.prototype.generateKey = function(network, cb) { |
|
|
|
if (!_.contains(['testnet', 'livenet'], network)) |
|
|
|
return cb('Invalid network'); |
|
|
|
|
|
|
|
this.storage.load(function(err, data) { |
|
|
|
if (data) |
|
|
|
this.storage.load(function(err, wcd) { |
|
|
|
if (wcd) |
|
|
|
return cb(self.storage.getName() + ' already contains a wallet'); |
|
|
|
|
|
|
|
var data = self._initData(network); |
|
|
|
self.storage.save(data, function(err) { |
|
|
|
var wcd = _initWcd(network); |
|
|
|
self.storage.save(wcd, function(err) { |
|
|
|
return cb(err, null); |
|
|
|
}); |
|
|
|
}); |
|
|
@ -289,10 +297,13 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb |
|
|
|
if (!_.contains(['testnet', 'livenet'], network)) |
|
|
|
return cb('Invalid network'); |
|
|
|
|
|
|
|
this.storage.load(function(err, data) { |
|
|
|
if (data) |
|
|
|
this.storage.load(function(err, wcd) { |
|
|
|
if (wcd && wcd.n) |
|
|
|
return cb(self.storage.getName() + ' already contains a wallet'); |
|
|
|
|
|
|
|
if (wcd && wcd.network && wcd.network != network) |
|
|
|
return cb('Storage ' + self.storage.getName() + ' is set to network:' + wcd.network); |
|
|
|
|
|
|
|
var walletPrivKey = new Bitcore.PrivateKey(); |
|
|
|
var args = { |
|
|
|
name: walletName, |
|
|
@ -308,11 +319,14 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb |
|
|
|
var walletId = body.walletId; |
|
|
|
|
|
|
|
var secret = WalletUtils.toSecret(walletId, walletPrivKey, network); |
|
|
|
var data = self._initData(network, walletPrivKey, m, n); |
|
|
|
self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[0], copayerName, |
|
|
|
|
|
|
|
wcd = wcd || _initWcd(network); |
|
|
|
_addWalletToWcd(wcd, walletPrivKey, m, n) |
|
|
|
|
|
|
|
self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[0], copayerName, |
|
|
|
function(err, wallet) { |
|
|
|
if (err) return cb(err); |
|
|
|
self.storage.save(data, function(err) { |
|
|
|
self.storage.save(wcd, function(err) { |
|
|
|
return cb(err, n > 1 ? secret : null); |
|
|
|
}); |
|
|
|
}); |
|
|
@ -323,16 +337,16 @@ API.prototype.createWallet = function(walletName, copayerName, m, n, network, cb |
|
|
|
|
|
|
|
API.prototype.reCreateWallet = function(walletName, cb) { |
|
|
|
var self = this; |
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var walletPrivKey = new Bitcore.PrivateKey(); |
|
|
|
var args = { |
|
|
|
name: walletName, |
|
|
|
m: data.m, |
|
|
|
n: data.n, |
|
|
|
m: wcd.m, |
|
|
|
n: wcd.n, |
|
|
|
pubKey: walletPrivKey.toPublicKey().toString(), |
|
|
|
network: data.network, |
|
|
|
network: wcd.network, |
|
|
|
}; |
|
|
|
var url = '/v1/wallets/'; |
|
|
|
self._doPostRequest(url, args, {}, function(err, body) { |
|
|
@ -340,11 +354,11 @@ API.prototype.reCreateWallet = function(walletName, cb) { |
|
|
|
|
|
|
|
var walletId = body.walletId; |
|
|
|
|
|
|
|
var secret = WalletUtils.toSecret(walletId, walletPrivKey, data.network); |
|
|
|
var secret = WalletUtils.toSecret(walletId, walletPrivKey, wcd.network); |
|
|
|
var i = 0; |
|
|
|
async.each(data.publicKeyRing, function(xpub, next) { |
|
|
|
async.each(wcd.publicKeyRing, function(xpub, next) { |
|
|
|
var copayerName = 'recovered Copayer #' + i; |
|
|
|
self._doJoinWallet(walletId, walletPrivKey, data.publicKeyRing[i++], copayerName, next); |
|
|
|
self._doJoinWallet(walletId, walletPrivKey, wcd.publicKeyRing[i++], copayerName, next); |
|
|
|
}, function(err) { |
|
|
|
return cb(err); |
|
|
|
}); |
|
|
@ -356,22 +370,22 @@ API.prototype.reCreateWallet = function(walletName, cb) { |
|
|
|
API.prototype.joinWallet = function(secret, copayerName, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this.storage.load(function(err, data) { |
|
|
|
if (data) |
|
|
|
return cb('Storage already contains a wallet'); |
|
|
|
this.storage.load(function(err, wcd) { |
|
|
|
if (wcd && wcd.n) |
|
|
|
return cb(self.storage.getName() + ' already contains a wallet'); |
|
|
|
|
|
|
|
try { |
|
|
|
var secretData = WalletUtils.fromSecret(secret); |
|
|
|
} catch (ex) { |
|
|
|
return cb(ex); |
|
|
|
} |
|
|
|
var data = self._initData(secretData.network, secretData.walletPrivKey); |
|
|
|
self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, data.publicKeyRing[0], copayerName, |
|
|
|
function(err, wallet) { |
|
|
|
wcd = wcd || _initWcd(secretData.network); |
|
|
|
|
|
|
|
self._doJoinWallet(secretData.walletId, secretData.walletPrivKey, wcd.publicKeyRing[0], copayerName, |
|
|
|
function(err, joinedWallet) { |
|
|
|
if (err) return cb(err); |
|
|
|
data.m = wallet.m; |
|
|
|
data.n = wallet.n; |
|
|
|
self.storage.save(data, cb); |
|
|
|
_addWalletToWcd(wcd, secretData.walletPrivKey, joinedWallet.m, joinedWallet.n); |
|
|
|
self.storage.save(wcd, cb); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -379,13 +393,13 @@ API.prototype.joinWallet = function(secret, copayerName, cb) { |
|
|
|
API.prototype.getStatus = function(cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._load(function(err, data) { |
|
|
|
this._load(function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var url = '/v1/wallets/'; |
|
|
|
self._doGetRequest(url, data, function(err, result) { |
|
|
|
_processTxps(result.pendingTxps, data.sharedEncryptingKey); |
|
|
|
return cb(err, result, data.copayerId); |
|
|
|
self._doGetRequest(url, wcd, function(err, result) { |
|
|
|
_processTxps(result.pendingTxps, wcd.sharedEncryptingKey); |
|
|
|
return cb(err, result, wcd.copayerId); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -404,36 +418,36 @@ API.prototype.sendTxProposal = function(opts, cb) { |
|
|
|
|
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!data.rwPrivKey) |
|
|
|
if (!wcd.rwPrivKey) |
|
|
|
return cb('No key to generate proposals'); |
|
|
|
|
|
|
|
var args = { |
|
|
|
toAddress: opts.toAddress, |
|
|
|
amount: opts.amount, |
|
|
|
message: _encryptMessage(opts.message, data.sharedEncryptingKey), |
|
|
|
message: _encryptMessage(opts.message, wcd.sharedEncryptingKey), |
|
|
|
}; |
|
|
|
var hash = WalletUtils.getProposalHash(args.toAddress, args.amount, args.message); |
|
|
|
args.proposalSignature = WalletUtils.signMessage(hash, data.rwPrivKey); |
|
|
|
args.proposalSignature = WalletUtils.signMessage(hash, wcd.rwPrivKey); |
|
|
|
log.debug('Generating & signing tx proposal hash -> Hash: ', hash, ' Signature: ', args.proposalSignature); |
|
|
|
|
|
|
|
var url = '/v1/txproposals/'; |
|
|
|
self._doPostRequest(url, args, data, cb); |
|
|
|
self._doPostRequest(url, args, wcd, cb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype.createAddress = function(cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var url = '/v1/addresses/'; |
|
|
|
self._doPostRequest(url, {}, data, function(err, address) { |
|
|
|
self._doPostRequest(url, {}, wcd, function(err, address) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!Verifier.checkAddress(data, address)) { |
|
|
|
if (!Verifier.checkAddress(wcd, address)) { |
|
|
|
return cb(new ServerCompromisedError('Server sent fake address')); |
|
|
|
} |
|
|
|
|
|
|
@ -449,16 +463,16 @@ API.prototype.createAddress = function(cb) { |
|
|
|
API.prototype.getMainAddresses = function(opts, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var url = '/v1/addresses/'; |
|
|
|
self._doGetRequest(url, data, function(err, addresses) { |
|
|
|
self._doGetRequest(url, wcd, function(err, addresses) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!opts.doNotVerify) { |
|
|
|
var fake = _.any(addresses, function(address) { |
|
|
|
return !Verifier.checkAddress(data, address); |
|
|
|
return !Verifier.checkAddress(wcd, address); |
|
|
|
}); |
|
|
|
if (fake) |
|
|
|
return cb(new ServerCompromisedError('Server sent fake address')); |
|
|
@ -475,15 +489,16 @@ API.prototype.history = function(limit, cb) { |
|
|
|
API.prototype.getBalance = function(cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
var url = '/v1/balance/'; |
|
|
|
self._doGetRequest(url, data, cb); |
|
|
|
self._doGetRequest(url, wcd, cb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* export |
|
|
|
* Export does not try to complete the wallet from the server. Exports the |
|
|
|
* wallet as it is now. |
|
|
|
* |
|
|
|
* @param opts.access =['full', 'readonly', 'readwrite'] |
|
|
|
*/ |
|
|
@ -493,11 +508,11 @@ API.prototype.export = function(opts, cb) { |
|
|
|
opts = opts || {}; |
|
|
|
var access = opts.access || 'full'; |
|
|
|
|
|
|
|
this._load(function(err, data) { |
|
|
|
this._load(function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
var v = []; |
|
|
|
|
|
|
|
var myXPubKey = (new Bitcore.HDPublicKey(data.xPrivKey)).toString(); |
|
|
|
var myXPubKey = (new Bitcore.HDPublicKey(wcd.xPrivKey)).toString(); |
|
|
|
|
|
|
|
_.each(WALLET_CRITICAL_DATA, function(k) { |
|
|
|
var d; |
|
|
@ -509,18 +524,18 @@ API.prototype.export = function(opts, cb) { |
|
|
|
|
|
|
|
// Skips own pub key IF priv key is exported
|
|
|
|
if (access == 'full' && k === 'publicKeyRing') { |
|
|
|
d = _.without(data[k], myXPubKey); |
|
|
|
d = _.without(wcd[k], myXPubKey); |
|
|
|
} else { |
|
|
|
d = data[k]; |
|
|
|
d = wcd[k]; |
|
|
|
} |
|
|
|
v.push(d); |
|
|
|
}); |
|
|
|
|
|
|
|
if (access != 'full') { |
|
|
|
v.push(data.copayerId); |
|
|
|
v.push(data.roPrivKey); |
|
|
|
v.push(wcd.copayerId); |
|
|
|
v.push(wcd.roPrivKey); |
|
|
|
if (access == 'readwrite') { |
|
|
|
v.push(data.rwPrivKey); |
|
|
|
v.push(wcd.rwPrivKey); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -532,39 +547,34 @@ API.prototype.export = function(opts, cb) { |
|
|
|
API.prototype.import = function(str, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this.storage.load(function(err, data) { |
|
|
|
if (data) |
|
|
|
this.storage.load(function(err, wcd) { |
|
|
|
if (wcd) |
|
|
|
return cb('Storage already contains a wallet'); |
|
|
|
|
|
|
|
data = {}; |
|
|
|
wcd = {}; |
|
|
|
|
|
|
|
var inData = JSON.parse(str); |
|
|
|
var i = 0; |
|
|
|
|
|
|
|
_.each(WALLET_CRITICAL_DATA.concat(WALLET_EXTRA_DATA), function(k) { |
|
|
|
data[k] = inData[i++]; |
|
|
|
wcd[k] = inData[i++]; |
|
|
|
}); |
|
|
|
|
|
|
|
if (data.xPrivKey) { |
|
|
|
var xpriv = new Bitcore.HDPrivateKey(data.xPrivKey); |
|
|
|
if (wcd.xPrivKey) { |
|
|
|
var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey); |
|
|
|
var xPubKey = new Bitcore.HDPublicKey(xpriv).toString(); |
|
|
|
data.publicKeyRing.unshift(xPubKey); |
|
|
|
data.copayerId = WalletUtils.xPubToCopayerId(xPubKey); |
|
|
|
data.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); |
|
|
|
data.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); |
|
|
|
wcd.publicKeyRing.unshift(xPubKey); |
|
|
|
wcd.copayerId = WalletUtils.xPubToCopayerId(xPubKey); |
|
|
|
wcd.roPrivKey = xpriv.derive('m/1/0').privateKey.toWIF(); |
|
|
|
wcd.rwPrivKey = xpriv.derive('m/1/1').privateKey.toWIF(); |
|
|
|
} |
|
|
|
|
|
|
|
var dataIsComplete = !!data.m; |
|
|
|
|
|
|
|
if (dataIsComplete) |
|
|
|
data.n = data.publicKeyRing.length; |
|
|
|
|
|
|
|
if (!data.publicKeyRing) |
|
|
|
return cb('Invalid source data'); |
|
|
|
if (!wcd.publicKeyRing) |
|
|
|
return cb('Invalid source wallet'); |
|
|
|
|
|
|
|
data.network = data.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; |
|
|
|
self.storage.save(data, function(err) { |
|
|
|
return cb(err, WalletUtils.accessFromData(data)); |
|
|
|
wcd.network = wcd.publicKeyRing[0].substr(0, 4) == 'tpub' ? 'testnet' : 'livenet'; |
|
|
|
self.storage.save(wcd, function(err) { |
|
|
|
return cb(err, WalletUtils.accessFromData(wcd)); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -578,14 +588,14 @@ API.prototype.parseTxProposals = function(txData, cb) { |
|
|
|
|
|
|
|
this._loadAndCheck({ |
|
|
|
toComplete: txData.toComplete |
|
|
|
}, function(err, data) { |
|
|
|
}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var txps = txData.txps; |
|
|
|
_processTxps(txps, data.sharedEncryptingKey); |
|
|
|
_processTxps(txps, wcd.sharedEncryptingKey); |
|
|
|
|
|
|
|
var fake = _.any(txps, function(txp) { |
|
|
|
return (!Verifier.checkTxProposal(data, txp)); |
|
|
|
return (!Verifier.checkTxProposal(wcd, txp)); |
|
|
|
}); |
|
|
|
|
|
|
|
if (fake) |
|
|
@ -607,20 +617,20 @@ API.prototype.parseTxProposals = function(txData, cb) { |
|
|
|
API.prototype.getTxProposals = function(opts, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
var url = '/v1/txproposals/'; |
|
|
|
self._doGetRequest(url, data, function(err, txps) { |
|
|
|
self._doGetRequest(url, wcd, function(err, txps) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var rawTxps; |
|
|
|
if (opts.getRawTxps) |
|
|
|
rawTxps = JSON.parse(JSON.stringify(txps)); |
|
|
|
|
|
|
|
_processTxps(txps, data.sharedEncryptingKey); |
|
|
|
_processTxps(txps, wcd.sharedEncryptingKey); |
|
|
|
|
|
|
|
var fake = _.any(txps, function(txp) { |
|
|
|
return (!opts.doNotVerify && !Verifier.checkTxProposal(data, txp)); |
|
|
|
return (!opts.doNotVerify && !Verifier.checkTxProposal(wcd, txp)); |
|
|
|
}); |
|
|
|
|
|
|
|
if (fake) |
|
|
@ -631,14 +641,14 @@ API.prototype.getTxProposals = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype._getSignaturesFor = function(txp, data) { |
|
|
|
API.prototype._getSignaturesFor = function(txp, wcd) { |
|
|
|
|
|
|
|
//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); |
|
|
|
var xpriv = new Bitcore.HDPrivateKey(wcd.xPrivKey, network); |
|
|
|
|
|
|
|
_.each(txp.inputs, function(i) { |
|
|
|
if (!derived[i.path]) { |
|
|
@ -671,24 +681,24 @@ API.prototype.getSignatures = function(txp, cb) { |
|
|
|
$.checkArgument(txp.creatorId); |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!Verifier.checkTxProposal(data, txp)) { |
|
|
|
if (!Verifier.checkTxProposal(wcd, txp)) { |
|
|
|
return cb(new ServerCompromisedError('Transaction proposal is invalid')); |
|
|
|
} |
|
|
|
|
|
|
|
return cb(null, self._getSignaturesFor(txp, data)); |
|
|
|
return cb(null, self._getSignaturesFor(txp, wcd)); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
API.prototype.getEncryptedWalletData = function(cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
var toComplete = JSON.stringify(_.pick(data, WALLET_AIRGAPPED_TOCOMPLETE)); |
|
|
|
return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(data.roPrivKey))); |
|
|
|
var toComplete = JSON.stringify(_.pick(wcd, WALLET_AIRGAPPED_TOCOMPLETE)); |
|
|
|
return cb(null, _encryptMessage(toComplete, WalletUtils.privateKeyToAESKey(wcd.roPrivKey))); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -699,21 +709,21 @@ API.prototype.signTxProposal = function(txp, cb) { |
|
|
|
|
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, function(err, data) { |
|
|
|
this._loadAndCheck({}, function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!Verifier.checkTxProposal(data, txp)) { |
|
|
|
if (!Verifier.checkTxProposal(wcd, txp)) { |
|
|
|
return cb(new ServerCompromisedError('Server sent fake transaction proposal')); |
|
|
|
} |
|
|
|
|
|
|
|
var signatures = txp.signatures || self._getSignaturesFor(txp, data); |
|
|
|
var signatures = txp.signatures || self._getSignaturesFor(txp, wcd); |
|
|
|
|
|
|
|
var url = '/v1/txproposals/' + txp.id + '/signatures/'; |
|
|
|
var args = { |
|
|
|
signatures: signatures |
|
|
|
}; |
|
|
|
|
|
|
|
self._doPostRequest(url, args, data, cb); |
|
|
|
self._doPostRequest(url, args, wcd, cb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -723,14 +733,14 @@ API.prototype.rejectTxProposal = function(txp, reason, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, |
|
|
|
function(err, data) { |
|
|
|
function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var url = '/v1/txproposals/' + txp.id + '/rejections/'; |
|
|
|
var args = { |
|
|
|
reason: _encryptMessage(reason, data.sharedEncryptingKey) || '', |
|
|
|
reason: _encryptMessage(reason, wcd.sharedEncryptingKey) || '', |
|
|
|
}; |
|
|
|
self._doPostRequest(url, args, data, cb); |
|
|
|
self._doPostRequest(url, args, wcd, cb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -738,11 +748,11 @@ API.prototype.broadcastTxProposal = function(txp, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
this._loadAndCheck({}, |
|
|
|
function(err, data) { |
|
|
|
function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
var url = '/v1/txproposals/' + txp.id + '/broadcast/'; |
|
|
|
self._doPostRequest(url, {}, data, cb); |
|
|
|
self._doPostRequest(url, {}, wcd, cb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
@ -751,10 +761,10 @@ API.prototype.broadcastTxProposal = function(txp, cb) { |
|
|
|
API.prototype.removeTxProposal = function(txp, cb) { |
|
|
|
var self = this; |
|
|
|
this._loadAndCheck({}, |
|
|
|
function(err, data) { |
|
|
|
function(err, wcd) { |
|
|
|
if (err) return cb(err); |
|
|
|
var url = '/v1/txproposals/' + txp.id; |
|
|
|
self._doRequest('delete', url, {}, data, cb); |
|
|
|
self._doRequest('delete', url, {}, wcd, cb); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|