@owstack/wallet-service
Version:
A service for multisignature HD wallets
552 lines (473 loc) • 17.9 kB
JavaScript
const chai = require('chai');
const sinon = require('sinon');
const should = chai.should();
const Service = require('../../');
const Services = {
BCH: Service.BCH.WalletService,
BTC: Service.BTC.WalletService,
LTC: Service.LTC.WalletService
};
const owsCommon = require('@owstack/ows-common');
const btcLib = require('@owstack/btc-lib');
const keyLib = require('@owstack/key-lib');
const async = require('async');
const Constants = owsCommon.Constants;
const Copayer = require('../../base-service/lib/model/copayer');
const ECDSA = keyLib.crypto.ECDSA;
const Hash = owsCommon.Hash;
const HDPrivateKey = keyLib.HDPrivateKey;
const HDPublicKey = keyLib.HDPublicKey;
const log = require('npmlog');
const PrivateKey = keyLib.PrivateKey;
const testConfig = require('config');
const TestData = require('../testdata');
const tingodb = require('tingodb')({memStore: true});
const Unit = btcLib.Unit;
const lodash = owsCommon.deps.lodash;
const atomicsAccessor = Unit().atomicsAccessor();
const storage = {};
const blockchainExplorer = {};
const useMongoDb = !!process.env.USE_MONGO_DB;
const helpers = {};
log.debug = log.verbose;
helpers.CLIENT_VERSION = 'bwc-2.0.0';
helpers.before = function (serviceName, cb) {
function getDb(cb) {
if (useMongoDb) {
const mongodb = require('mongodb');
mongodb.MongoClient.connect(testConfig.storageOpts.mongoDb.uri, function (err, db) {
if (err) {
throw err;
}
return cb(db);
});
} else {
const db = new tingodb.Db('./db/test', {});
return cb(db);
}
}
getDb(function (db) {
storage[serviceName] = new Services[serviceName].Storage(null, {
db: db
});
return cb();
});
};
helpers.beforeEach = function (serviceName, cb) {
if (!storage[serviceName].opts.db) {
return cb('Error - no storage for test');
}
storage[serviceName].opts.db.dropDatabase(function (err) {
if (err) {
return cb(err);
}
blockchainExplorer[serviceName] = sinon.stub();
cb(null, {
blockchainExplorer: helpers.getBlockchainExplorer(serviceName),
request: sinon.stub(),
storage: helpers.getStorage(serviceName)
});
});
};
helpers.after = function (server, cb) {
server.shutDown(cb);
};
helpers.getBlockchainExplorer = function (serviceName) {
return blockchainExplorer[serviceName];
};
helpers._getServiceName = function (server) {
return server.getServiceInfo().currency;
};
helpers.getStorage = function (serviceName) {
return storage[serviceName];
};
helpers.signMessage = function (serviceName, text, privKey) {
const priv = new PrivateKey(privKey);
const hash = Services[serviceName].Common.Utils.hashMessage(text);
return ECDSA.sign(hash, priv, 'little').toString();
};
helpers.signRequestPubKey = function (serviceName, requestPubKey, xPrivKey) {
const priv = new HDPrivateKey(xPrivKey).deriveChild(Constants.PATHS.REQUEST_KEY_AUTH).privateKey;
return helpers.signMessage(serviceName, requestPubKey, priv);
};
helpers.getAuthServer = function (serviceName, copayerId, cb) {
const Server = Services[serviceName].Server;
const verifyStub = sinon.stub(Server.prototype, '_verifySignature');
verifyStub.returns(true);
const opts = {
blockchainExplorer: helpers.getBlockchainExplorer(serviceName),
request: sinon.stub(),
storage: helpers.getStorage(serviceName),
force: true
};
Server.getInstanceWithAuth(opts, testConfig, {
copayerId: copayerId,
message: 'dummy',
signature: 'dummy'
}, function (err, server) {
verifyStub.restore();
if (err || !server) {
throw new Error(`Could not login as copayerId ${ copayerId } err: ${ err}`);
}
return cb(server);
});
};
// helpers._generateCopayersTestData = function (n) {
// console.log('var copayers = [');
// lodash.each(lodash.range(n), function (c) {
// const xpriv = new HDPrivateKey();
// const xpub = HDPublicKey(xpriv);
//
// const xpriv_45H = xpriv.deriveChild(45, true);
// const xpub_45H = HDPublicKey(xpriv_45H);
// const id45 = Copayer._xPubToCopayerId(xpub_45H.toString());
//
// const xpriv_44H_0H_0H = xpriv.deriveChild(44, true).deriveChild(0, true).deriveChild(0, true);
// const xpub_44H_0H_0H = HDPublicKey(xpriv_44H_0H_0H);
// const id44 = Copayer._xPubToCopayerId(xpub_44H_0H_0H.toString());
//
// const xpriv_1H = xpriv.deriveChild(1, true);
// const xpub_1H = HDPublicKey(xpriv_1H);
// const priv = xpriv_1H.deriveChild(0).privateKey;
// const pub = xpub_1H.deriveChild(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 (serviceName, opts) {
const Server = Services[serviceName].Server;
const hash = Server._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey);
opts.copayerSignature = helpers.signMessage(serviceName, hash, TestData.keyPair.priv);
return opts;
};
helpers.createAndJoinWallet = function (serviceName, m, n, opts, cb) {
const Server = Services[serviceName].Server;
if (lodash.isFunction(opts)) {
cb = opts;
opts = {};
}
opts = opts || {};
const serverOpts = {
blockchainExplorer: helpers.getBlockchainExplorer(serviceName),
storage: helpers.getStorage(serviceName),
request: sinon.stub(),
force: true
};
new Server(serverOpts, testConfig, function (server) {
const copayerIds = [];
const offset = opts.offset || 0;
const walletOpts = {
name: 'a wallet',
m: m,
n: n,
pubKey: TestData.keyPair.pub,
singleAddress: !!opts.singleAddress
};
if (lodash.isBoolean(opts.supportBIP44AndP2PKH)) {
walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH;
}
server.createWallet(walletOpts, function (err, walletId) {
if (err) {
return cb(err);
}
async.each(lodash.range(n), function (i, cb) {
const copayerData = TestData.copayers[i + offset];
const copayerOpts = helpers.getSignedCopayerOpts(serviceName, {
walletId: walletId,
name: `copayer ${ i + 1}`,
xPubKey: (lodash.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H,
requestPubKey: copayerData.pubKey_1H_0,
customData: `custom data ${ i + 1}`,
});
if (lodash.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 cb('Could not generate wallet');
}
helpers.getAuthServer(serviceName, copayerIds[0], function (s) {
s.getWallet({}, function (err, w) {
cb(s, w);
});
});
});
});
});
};
helpers.randomTXID = function () {
return Hash.sha256(Buffer.from(`${Math.random() * 100000 }`)).toString('hex');
};
helpers.toAtomic = function (serviceName, standards) {
if (lodash.isArray(standards)) {
return lodash.map(standards, helpers.toAtomic);
} else {
return Services[serviceName].Common.Utils.strip(Unit.fromStandardUnit(standards).toAtomicUnit());
}
};
helpers._parseAmount = function (serviceName, str) {
const result = {
amount: +0,
confirmations: lodash.random(6, 100),
};
if (lodash.isNumber(str)) {
str = str.toString();
}
const re = /^((?:\d+c)|u)?\s*([\d.]+)\s*(BTC|bit|satoshi)?$/;
const match = str.match(re);
if (!match) {
throw new Error(`Could not parse amount ${ str}`);
}
if (match[1]) {
if (match[1] == 'u') {
result.confirmations = 0;
}
if (lodash.endsWith(match[1], 'c')) {
result.confirmations = +match[1].slice(0, -1);
}
}
switch (match[3]) {
default:
case 'BTC':
result.amount = Services[serviceName].Common.Utils.strip(+match[2] * 1e8);
break;
case 'bit':
result.amount = Services[serviceName].Common.Utils.strip(+match[2] * 1e2);
break;
case 'satoshi':
result.amount = Services[serviceName].Common.Utils.strip(+match[2]);
break;
}
return result;
};
helpers.stubUtxos = function (server, wallet, amounts, opts, cb) {
const serviceName = helpers._getServiceName(server);
if (lodash.isFunction(opts)) {
cb = opts;
opts = {};
}
opts = opts || {};
if (!helpers._utxos) {
helpers._utxos = {};
}
async.waterfall([
function (next) {
if (opts.addresses) {
return next(null, [].concat(opts.addresses));
}
async.mapSeries(lodash.range(0, amounts.length > 2 ? 2 : 1), function (i, next) {
server.createAddress({}, next);
}, next);
},
function (addresses, next) {
addresses.should.not.be.empty;
const utxos = lodash.compact(lodash.map([].concat(amounts), function (amount, i) {
const parsed = helpers._parseAmount(serviceName, amount);
if (parsed.amount <= 0) {
return null;
}
const address = addresses[i % addresses.length];
let scriptPubKey;
switch (wallet.addressType) {
case Constants.SCRIPT_TYPES.P2SH:
scriptPubKey = btcLib.Script.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut();
break;
case Constants.SCRIPT_TYPES.P2PKH:
scriptPubKey = btcLib.Script.buildPublicKeyHashOut(address.address);
break;
}
should.exist(scriptPubKey);
const res = {
txid: helpers.randomTXID(),
vout: lodash.random(0, 10),
scriptPubKey: scriptPubKey.toBuffer().toString('hex'),
address: address.address,
confirmations: parsed.confirmations,
publicKeys: address.publicKeys,
};
res[atomicsAccessor] = parsed.amount;
return res;
}));
if (opts.keepUtxos) {
helpers._utxos = helpers._utxos.concat(utxos);
} else {
helpers._utxos = utxos;
}
helpers.getBlockchainExplorer(serviceName).getUtxos = function (addresses, cb) {
const selected = lodash.filter(helpers._utxos, function (utxo) {
return lodash.includes(addresses, utxo.address);
});
return cb(null, selected);
};
return next();
},
], function (err) {
should.not.exist(err);
return cb(helpers._utxos);
});
};
helpers.stubBroadcast = function (serviceName) {
helpers.getBlockchainExplorer(serviceName).broadcast = sinon.stub().callsArgWith(1, null, '112233');
helpers.getBlockchainExplorer(serviceName).getTransaction = sinon.stub().callsArgWith(1, null, null);
};
helpers.stubHistory = function (serviceName, txs) {
const totalItems = txs.length;
helpers.getBlockchainExplorer(serviceName).getTransactions = function (addresses, from, to, cb) {
const MAX_BATCH_SIZE = 100;
const nbTxs = txs.length;
if (lodash.isUndefined(from) && lodash.isUndefined(to)) {
from = 0;
to = MAX_BATCH_SIZE;
}
if (!lodash.isUndefined(from) && lodash.isUndefined(to)) {
to = from + MAX_BATCH_SIZE;
}
if (!lodash.isUndefined(from) && !lodash.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;
}
const page = txs.slice(from, to);
return cb(null, page, totalItems);
};
};
helpers.stubFeeLevels = function (serviceName, levels) {
helpers.getBlockchainExplorer(serviceName).estimateFee = function (nbBlocks, cb) {
const result = lodash.fromPairs(lodash.map(lodash.pick(levels, nbBlocks), function (fee, n) {
return [+n, fee > 0 ? fee / 1e8 : fee];
}));
return cb(null, result);
};
};
helpers.stubAddressActivity = function (serviceName, activeAddresses) {
helpers.getBlockchainExplorer(serviceName).getAddressActivity = function (address, cb) {
return cb(null, lodash.includes(activeAddresses, address));
};
};
helpers.clientSign = function (txp, derivedXPrivKey) {
//Derive proper key to sign, for each input
const privs = [];
const derived = {};
const xpriv = new HDPrivateKey(derivedXPrivKey, txp.networkName);
lodash.each(txp.inputs, function (i) {
if (!derived[i.path]) {
derived[i.path] = xpriv.deriveChild(i.path).privateKey;
privs.push(derived[i.path]);
}
});
const t = txp.getTx();
let signatures = lodash.map(privs, function (priv) {
return t.getSignatures(priv);
});
signatures = lodash.map(lodash.sortBy(lodash.flatten(signatures), 'inputIndex'), function (s) {
return s.signature.toDER().toString('hex');
});
return signatures;
};
helpers.getProposalSignatureOpts = function (serviceName, txp, signingKey) {
const raw = txp.getRawTx();
const proposalSignature = helpers.signMessage(serviceName, raw, signingKey);
return {
txProposalId: txp.id,
proposalSignature: proposalSignature,
};
};
helpers.createAddresses = function (server, wallet, main, change, cb) {
// var clock = sinon.useFakeTimers('Date');
async.mapSeries(lodash.range(main + change), function (i, next) {
// clock.tick(1000);
const address = wallet.createAddress(i >= main);
server.getStorage().storeAddressAndWallet(wallet, address, function (err) {
next(err, address);
});
}, function (err, addresses) {
should.not.exist(err);
// clock.restore();
return cb(lodash.take(addresses, main), lodash.takeRight(addresses, change));
});
};
helpers.createAndPublishTx = function (server, txOpts, signingKey, cb) {
const serviceName = helpers._getServiceName(server);
server.createTx(txOpts, function (err, txp) {
const publishOpts = helpers.getProposalSignatureOpts(serviceName, txp, signingKey);
server.publishTx(publishOpts, function (err) {
should.not.exist(err);
return cb(txp);
});
});
};
helpers.historyCacheTest = function (items) {
const template = {
txid: 'fad88682ccd2ff34cac6f7355fe9ecd8addd9ef167e3788455972010e0d9d0de',
vin: [{
txid: '0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04',
vout: 0,
n: 0,
addr: '2NAVFnsHqy5JvqDJydbHPx393LFqFFBQ89V',
valueAtomic: 45753,
value: 0.00045753,
}],
vout: [{
value: '0.00011454',
n: 0,
scriptPubKey: {
addresses: [
'2N7GT7XaN637eBFMmeczton2aZz5rfRdZso'
]
}
}, {
value: '0.00020000',
n: 1,
scriptPubKey: {
addresses: [
'mq4D3Va5mYHohMEHrgHNGzCjKhBKvuEhPE'
]
}
}],
confirmations: 1,
blockheight: 423499,
time: 1424472242,
blocktime: 1424472242,
valueOut: 0.00031454,
valueIn: 0.00045753,
fees: 0.00014299
};
const ret = [];
lodash.each(lodash.range(0, items), function (i) {
const t = lodash.clone(template);
t.txid = `txid:${ i}`;
t.confirmations = items - i - 1;
t.blockheight = i;
t.time = t.blocktime = i;
ret.unshift(t);
});
return ret;
};
module.exports = helpers;