Ivan Socolsky
9 years ago
2 changed files with 383 additions and 352 deletions
@ -0,0 +1,380 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
var _ = require('lodash'); |
||||
|
var async = require('async'); |
||||
|
|
||||
|
var chai = require('chai'); |
||||
|
var sinon = require('sinon'); |
||||
|
var should = chai.should(); |
||||
|
var log = require('npmlog'); |
||||
|
log.debug = log.verbose; |
||||
|
|
||||
|
var Bitcore = require('bitcore-lib'); |
||||
|
|
||||
|
var Common = require('../../lib/common'); |
||||
|
var Utils = Common.Utils; |
||||
|
var Constants = Common.Constants; |
||||
|
var Defaults = Common.Defaults; |
||||
|
|
||||
|
var Model = require('../../lib/model'); |
||||
|
var WalletService = require('../../lib/server'); |
||||
|
var TestData = require('../testdata'); |
||||
|
|
||||
|
var blockchainExplorer; |
||||
|
|
||||
|
var helpers = {}; |
||||
|
|
||||
|
helpers.setBlockchainExplorer = function(bce) { |
||||
|
blockchainExplorer = bce; |
||||
|
}; |
||||
|
|
||||
|
helpers.signMessage = function(text, privKey) { |
||||
|
var priv = new Bitcore.PrivateKey(privKey); |
||||
|
var hash = Utils.hashMessage(text); |
||||
|
return Bitcore.crypto.ECDSA.sign(hash, priv, 'little').toString(); |
||||
|
}; |
||||
|
|
||||
|
helpers.signRequestPubKey = function(requestPubKey, xPrivKey) { |
||||
|
var priv = new Bitcore.HDPrivateKey(xPrivKey).derive(Constants.PATHS.REQUEST_KEY_AUTH).privateKey; |
||||
|
return helpers.signMessage(requestPubKey, priv); |
||||
|
}; |
||||
|
|
||||
|
helpers.getAuthServer = function(copayerId, cb) { |
||||
|
var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); |
||||
|
verifyStub.returns(true); |
||||
|
WalletService.getInstanceWithAuth({ |
||||
|
copayerId: copayerId, |
||||
|
message: 'dummy', |
||||
|
signature: 'dummy', |
||||
|
clientVersion: 'bwc-0.1.0', |
||||
|
}, function(err, server) { |
||||
|
verifyStub.restore(); |
||||
|
if (err || !server) throw new Error('Could not login as copayerId ' + copayerId); |
||||
|
return cb(server); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
helpers._generateCopayersTestData = function(n) { |
||||
|
console.log('var copayers = ['); |
||||
|
_.each(_.range(n), function(c) { |
||||
|
var xpriv = new Bitcore.HDPrivateKey(); |
||||
|
var xpub = Bitcore.HDPublicKey(xpriv); |
||||
|
|
||||
|
var xpriv_45H = xpriv.derive(45, true); |
||||
|
var xpub_45H = Bitcore.HDPublicKey(xpriv_45H); |
||||
|
var id45 = Copayer._xPubToCopayerId(xpub_45H.toString()); |
||||
|
|
||||
|
var xpriv_44H_0H_0H = xpriv.derive(44, true).derive(0, true).derive(0, true); |
||||
|
var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H); |
||||
|
var id44 = Copayer._xPubToCopayerId(xpub_44H_0H_0H.toString()); |
||||
|
|
||||
|
var xpriv_1H = xpriv.derive(1, true); |
||||
|
var xpub_1H = Bitcore.HDPublicKey(xpriv_1H); |
||||
|
var priv = xpriv_1H.derive(0).privateKey; |
||||
|
var pub = xpub_1H.derive(0).publicKey; |
||||
|
|
||||
|
console.log('{id44: ', "'" + id44 + "',"); |
||||
|
console.log('id45: ', "'" + id45 + "',"); |
||||
|
console.log('xPrivKey: ', "'" + xpriv.toString() + "',"); |
||||
|
console.log('xPubKey: ', "'" + xpub.toString() + "',"); |
||||
|
console.log('xPrivKey_45H: ', "'" + xpriv_45H.toString() + "',"); |
||||
|
console.log('xPubKey_45H: ', "'" + xpub_45H.toString() + "',"); |
||||
|
console.log('xPrivKey_44H_0H_0H: ', "'" + xpriv_44H_0H_0H.toString() + "',"); |
||||
|
console.log('xPubKey_44H_0H_0H: ', "'" + xpub_44H_0H_0H.toString() + "',"); |
||||
|
console.log('xPrivKey_1H: ', "'" + xpriv_1H.toString() + "',"); |
||||
|
console.log('xPubKey_1H: ', "'" + xpub_1H.toString() + "',"); |
||||
|
console.log('privKey_1H_0: ', "'" + priv.toString() + "',"); |
||||
|
console.log('pubKey_1H_0: ', "'" + pub.toString() + "'},"); |
||||
|
}); |
||||
|
console.log('];'); |
||||
|
}; |
||||
|
|
||||
|
helpers.getSignedCopayerOpts = function(opts) { |
||||
|
var hash = WalletService._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey); |
||||
|
opts.copayerSignature = helpers.signMessage(hash, TestData.keyPair.priv); |
||||
|
return opts; |
||||
|
}; |
||||
|
|
||||
|
helpers.createAndJoinWallet = function(m, n, opts, cb) { |
||||
|
if (_.isFunction(opts)) { |
||||
|
cb = opts; |
||||
|
opts = {}; |
||||
|
} |
||||
|
opts = opts || {}; |
||||
|
|
||||
|
var server = new WalletService(); |
||||
|
var copayerIds = []; |
||||
|
var offset = opts.offset || 0; |
||||
|
|
||||
|
var walletOpts = { |
||||
|
name: 'a wallet', |
||||
|
m: m, |
||||
|
n: n, |
||||
|
pubKey: TestData.keyPair.pub, |
||||
|
}; |
||||
|
if (_.isBoolean(opts.supportBIP44AndP2PKH)) |
||||
|
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; |
||||
|
|
||||
|
server.createWallet(walletOpts, function(err, walletId) { |
||||
|
if (err) return cb(err); |
||||
|
|
||||
|
async.each(_.range(n), function(i, cb) { |
||||
|
var copayerData = TestData.copayers[i + offset]; |
||||
|
var copayerOpts = helpers.getSignedCopayerOpts({ |
||||
|
walletId: walletId, |
||||
|
name: 'copayer ' + (i + 1), |
||||
|
xPubKey: (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H, |
||||
|
requestPubKey: copayerData.pubKey_1H_0, |
||||
|
customData: 'custom data ' + (i + 1), |
||||
|
}); |
||||
|
if (_.isBoolean(opts.supportBIP44AndP2PKH)) |
||||
|
copayerOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; |
||||
|
|
||||
|
server.joinWallet(copayerOpts, function(err, result) { |
||||
|
should.not.exist(err); |
||||
|
copayerIds.push(result.copayerId); |
||||
|
return cb(err); |
||||
|
}); |
||||
|
}, function(err) { |
||||
|
if (err) return new Error('Could not generate wallet'); |
||||
|
helpers.getAuthServer(copayerIds[0], function(s) { |
||||
|
s.getWallet({}, function(err, w) { |
||||
|
cb(s, w); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
helpers.randomTXID = function() { |
||||
|
return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');; |
||||
|
}; |
||||
|
|
||||
|
helpers.toSatoshi = function(btc) { |
||||
|
if (_.isArray(btc)) { |
||||
|
return _.map(btc, helpers.toSatoshi); |
||||
|
} else { |
||||
|
return Utils.strip(btc * 1e8); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
helpers.stubUtxos = function(server, wallet, amounts, cb) { |
||||
|
async.mapSeries(_.range(0, amounts.length > 2 ? 2 : 1), function(i, next) { |
||||
|
server.createAddress({}, next); |
||||
|
}, function(err, addresses) { |
||||
|
should.not.exist(err); |
||||
|
addresses.should.not.be.empty; |
||||
|
var utxos = _.map([].concat(amounts), function(amount, i) { |
||||
|
var address = addresses[i % addresses.length]; |
||||
|
var confirmations; |
||||
|
if (_.isString(amount) && _.startsWith(amount, 'u')) { |
||||
|
amount = parseFloat(amount.substring(1)); |
||||
|
confirmations = 0; |
||||
|
} else { |
||||
|
confirmations = Math.floor(Math.random() * 100 + 1); |
||||
|
} |
||||
|
|
||||
|
var scriptPubKey; |
||||
|
switch (wallet.addressType) { |
||||
|
case Constants.SCRIPT_TYPES.P2SH: |
||||
|
scriptPubKey = Bitcore.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); |
||||
|
break; |
||||
|
case Constants.SCRIPT_TYPES.P2PKH: |
||||
|
scriptPubKey = Bitcore.Script.buildPublicKeyHashOut(address.address); |
||||
|
break; |
||||
|
} |
||||
|
should.exist(scriptPubKey); |
||||
|
|
||||
|
return { |
||||
|
txid: helpers.randomTXID(), |
||||
|
vout: Math.floor(Math.random() * 10 + 1), |
||||
|
satoshis: helpers.toSatoshi(amount).toString(), |
||||
|
scriptPubKey: scriptPubKey.toBuffer().toString('hex'), |
||||
|
address: address.address, |
||||
|
confirmations: confirmations, |
||||
|
}; |
||||
|
}); |
||||
|
blockchainExplorer.getUnspentUtxos = function(addresses, cb) { |
||||
|
var selected = _.filter(utxos, function(utxo) { |
||||
|
return _.contains(addresses, utxo.address); |
||||
|
}); |
||||
|
return cb(null, selected); |
||||
|
}; |
||||
|
|
||||
|
return cb(utxos); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
helpers.stubBroadcast = function(thirdPartyBroadcast) { |
||||
|
blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, null, '112233'); |
||||
|
blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null); |
||||
|
}; |
||||
|
|
||||
|
helpers.stubHistory = function(txs) { |
||||
|
blockchainExplorer.getTransactions = function(addresses, from, to, cb) { |
||||
|
var MAX_BATCH_SIZE = 100; |
||||
|
var nbTxs = txs.length; |
||||
|
|
||||
|
if (_.isUndefined(from) && _.isUndefined(to)) { |
||||
|
from = 0; |
||||
|
to = MAX_BATCH_SIZE; |
||||
|
} |
||||
|
if (!_.isUndefined(from) && _.isUndefined(to)) |
||||
|
to = from + MAX_BATCH_SIZE; |
||||
|
|
||||
|
if (!_.isUndefined(from) && !_.isUndefined(to) && to - from > MAX_BATCH_SIZE) |
||||
|
to = from + MAX_BATCH_SIZE; |
||||
|
|
||||
|
if (from < 0) from = 0; |
||||
|
if (to < 0) to = 0; |
||||
|
if (from > nbTxs) from = nbTxs; |
||||
|
if (to > nbTxs) to = nbTxs; |
||||
|
|
||||
|
var page = txs.slice(from, to); |
||||
|
return cb(null, page); |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
helpers.stubFeeLevels = function(levels) { |
||||
|
blockchainExplorer.estimateFee = function(nbBlocks, cb) { |
||||
|
var result = _.zipObject(_.map(_.pick(levels, nbBlocks), function(fee, n) { |
||||
|
return [+n, fee > 0 ? fee / 1e8 : fee]; |
||||
|
})); |
||||
|
return cb(null, result); |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
helpers.stubAddressActivity = function(activeAddresses) { |
||||
|
blockchainExplorer.getAddressActivity = function(address, cb) { |
||||
|
return cb(null, _.contains(activeAddresses, address)); |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
helpers.clientSign = function(txp, xPrivKey) { |
||||
|
var self = this; |
||||
|
|
||||
|
function getBaseAddressDerivationPath(derivationStrategy, network, account) { |
||||
|
if (derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45) return "m/45'"; |
||||
|
return "m/44'/" + (network == 'livenet' ? "0'" : "1'") + "/" + account + "'"; |
||||
|
}; |
||||
|
|
||||
|
function deriveXPrivFromMaster(masterXPriv, derivationStrategy, network, account) { |
||||
|
var path = getBaseAddressDerivationPath(derivationStrategy, network, account || 0); |
||||
|
return new Bitcore.HDPrivateKey(masterXPriv, network).derive(path); |
||||
|
}; |
||||
|
|
||||
|
//Derive proper key to sign, for each input
|
||||
|
var privs = []; |
||||
|
var derived = {}; |
||||
|
|
||||
|
var network = new Bitcore.Address(txp.changeAddress.address).network.name; |
||||
|
var xpriv = deriveXPrivFromMaster(xPrivKey, txp.derivationStrategy, 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 = txp.getBitcoreTx(); |
||||
|
|
||||
|
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; |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
helpers.createProposalOptsLegacy = function(toAddress, amount, message, signingKey, feePerKb) { |
||||
|
var opts = { |
||||
|
toAddress: toAddress, |
||||
|
amount: helpers.toSatoshi(amount), |
||||
|
message: message, |
||||
|
proposalSignature: null, |
||||
|
}; |
||||
|
if (feePerKb) opts.feePerKb = feePerKb; |
||||
|
|
||||
|
var hash = WalletService._getProposalHash(toAddress, opts.amount, message); |
||||
|
|
||||
|
try { |
||||
|
opts.proposalSignature = helpers.signMessage(hash, signingKey); |
||||
|
} catch (ex) {} |
||||
|
|
||||
|
return opts; |
||||
|
}; |
||||
|
|
||||
|
helpers.createSimpleProposalOpts = function(toAddress, amount, signingKey, opts) { |
||||
|
var outputs = [{ |
||||
|
toAddress: toAddress, |
||||
|
amount: amount, |
||||
|
}]; |
||||
|
return helpers.createProposalOpts(Model.TxProposal.Types.SIMPLE, outputs, signingKey, opts); |
||||
|
}; |
||||
|
|
||||
|
helpers.createProposalOpts = function(type, outputs, signingKey, moreOpts) { |
||||
|
_.each(outputs, function(output) { |
||||
|
output.amount = helpers.toSatoshi(output.amount); |
||||
|
}); |
||||
|
|
||||
|
var opts = { |
||||
|
type: type, |
||||
|
proposalSignature: null |
||||
|
}; |
||||
|
|
||||
|
if (moreOpts) { |
||||
|
moreOpts = _.chain(moreOpts) |
||||
|
.pick(['feePerKb', 'customData', 'message']) |
||||
|
.value(); |
||||
|
opts = _.assign(opts, moreOpts); |
||||
|
} |
||||
|
|
||||
|
opts = _.defaults(opts, { |
||||
|
message: null |
||||
|
}); |
||||
|
|
||||
|
var hash; |
||||
|
if (type == Model.TxProposal.Types.SIMPLE) { |
||||
|
opts.toAddress = outputs[0].toAddress; |
||||
|
opts.amount = outputs[0].amount; |
||||
|
hash = WalletService._getProposalHash(opts.toAddress, opts.amount, |
||||
|
opts.message, opts.payProUrl); |
||||
|
} else if (type == Model.TxProposal.Types.MULTIPLEOUTPUTS) { |
||||
|
opts.outputs = outputs; |
||||
|
var header = { |
||||
|
outputs: outputs, |
||||
|
message: opts.message, |
||||
|
payProUrl: opts.payProUrl |
||||
|
}; |
||||
|
hash = WalletService._getProposalHash(header); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
opts.proposalSignature = helpers.signMessage(hash, signingKey); |
||||
|
} catch (ex) {} |
||||
|
|
||||
|
return opts; |
||||
|
}; |
||||
|
|
||||
|
helpers.createAddresses = function(server, wallet, main, change, cb) { |
||||
|
var clock = sinon.useFakeTimers(Date.now(), 'Date'); |
||||
|
async.map(_.range(main + change), function(i, next) { |
||||
|
clock.tick(1000); |
||||
|
var address = wallet.createAddress(i >= main); |
||||
|
server.storage.storeAddressAndWallet(wallet, address, function(err) { |
||||
|
next(err, address); |
||||
|
}); |
||||
|
}, function(err, addresses) { |
||||
|
if (err) throw new Error('Could not generate addresses'); |
||||
|
clock.restore(); |
||||
|
return cb(_.take(addresses, main), _.takeRight(addresses, change)); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
module.exports = helpers; |
Loading…
Reference in new issue