UNPKG

@abcpros/bitcore-wallet-service

Version:
724 lines (606 loc) 22.3 kB
'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 config = require('../test-config'); // var tingodb = require('tingodb')({ // memStore: true // }); var Bitcore = require('@abcpros/bitcore-lib'); var Bitcore_ = { btc: Bitcore, bch: require('@abcpros/bitcore-lib-cash'), bcha: require('@abcpros/bitcore-lib-cash'), xec: require('@abcpros/bitcore-lib-xec'), doge: require('@abcpros/bitcore-lib-doge'), xpi: require('@abcpros/bitcore-lib-xpi'), ltc: require('@abcpros/bitcore-lib-ltc') }; var { ChainService } = require('../../ts_build/lib/chain/index'); var Common = require('../../ts_build/lib/common'); var Utils = Common.Utils; var Constants = Common.Constants; var Defaults = Common.Defaults; var { Storage } = require('../../ts_build/lib/storage'); var { WalletService } = require('../../ts_build/lib/server'); var Model = require('../../ts_build/lib/model'); var TestData = require('../testdata'); var storage, blockchainExplorer; // tinodb not longer supported var useMongoDb = true; // !!process.env.USE_MONGO_DB; const CWC = require('@abcpros/crypto-wallet-core'); var helpers = {}; helpers.CLIENT_VERSION = 'bwc-2.0.0'; helpers.before = function(cb) { function getDb(cb) { if (useMongoDb) { var mongodb = require('mongodb'); mongodb.MongoClient.connect(config.mongoDb.uri, { useUnifiedTopology: true }, function(err, client) { if (err) throw err; return cb(client.db(config.mongoDb.dbname)); }); } else { throw "tingodb not longer supported"; //var db = new tingodb.Db('./db/test', {}); //return cb(db); } } getDb(function(db) { storage = new Storage({ db: db }); Storage.createIndexes(db); let be = blockchainExplorer = sinon.stub(); be.register = sinon.stub().callsArgWith(1, null, null); be.addAddresses = sinon.stub().callsArgWith(2, null, null); be.getAddressUtxos = sinon.stub().callsArgWith(2, null, []); be.getCheckData = sinon.stub().callsArgWith(1, null, {sum: 100}); be.getUtxos = sinon.stub().callsArgWith(1, null,[]); be.getBlockchainHeight = sinon.stub().callsArgWith(0, null, 1000, 'hash'); be.estimateGas = sinon.stub().callsArgWith(1, null, Defaults.MIN_GAS_LIMIT); be.getBalance = sinon.stub().callsArgWith(1, null, {unconfirmed:0, confirmed: '10000000000', balance: '10000000000' }); // just a number >0 (xrp does not accept 0) be.getTransactionCount = sinon.stub().callsArgWith(1, null, '5'); var opts = { storage: storage, blockchainExplorer: blockchainExplorer, request: sinon.stub() }; WalletService.initialize(opts, function() { return cb(opts); }); }); }; helpers.beforeEach = function(cb) { if (!storage.db) return cb(); // Left overs to be initalized let be = blockchainExplorer; be.register = sinon.stub().callsArgWith(1, null, null); be.addAddresses = sinon.stub().callsArgWith(2, null, null); // TODO const collections = { WALLETS: 'wallets', TXS: 'txs', ADDRESSES: 'addresses', NOTIFICATIONS: 'notifications', COPAYERS_LOOKUP: 'copayers_lookup', PREFERENCES: 'preferences', EMAIL_QUEUE: 'email_queue', CACHE: 'cache', FIAT_RATES2: 'fiat_rates2', TX_NOTES: 'tx_notes', SESSIONS: 'sessions', PUSH_NOTIFICATION_SUBS: 'push_notification_subs', TX_CONFIRMATION_SUBS: 'tx_confirmation_subs', LOCKS: 'locks' }; async.each(_.values(collections), (x, icb)=> { storage.db.collection(x).deleteMany({}, icb); }, (err) => { should.not.exist(err); var opts = { storage: storage, blockchainExplorer: blockchainExplorer, request: sinon.stub() }; WalletService.initialize(opts, function() { return cb(opts); }); }); }; helpers.after = function(cb) { WalletService.shutDown(() => { setImmediate(cb); }); }; helpers.getBlockchainExplorer = function() { return blockchainExplorer; }; helpers.getStorage = function() { return storage; }; helpers.signMessage = function(message, privKey) { var priv = new Bitcore.PrivateKey(privKey); const flattenedMessage = _.isArray(message)? _.join(message) : message; var hash = Utils.hashMessage(flattenedMessage); return Bitcore.crypto.ECDSA.sign(hash, priv, 'little').toString(); }; helpers.signRequestPubKey = function(requestPubKey, xPrivKey) { var priv = new Bitcore.HDPrivateKey(xPrivKey).deriveChild(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: helpers.CLIENT_VERSION, }, 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() { var xPrivKeys = ['xprv9s21ZrQH143K2n4rV4AtAJFptEmd1tNMKCcSyQBCSuN5eq1dCUhcv6KQJS49joRxu8NNdFxy8yuwTtzCPNYUZvVGC7EPRm2st2cvE7oyTbB', 'xprv9s21ZrQH143K3BwkLceWNLUsgES15JoZuv8BZfnmDRcCGtDooUAPhY8KovhCWcRLXUun5AYL5vVtUNRrmPEibtfk9ongxAGLXZzEHifpvwZ', 'xprv9s21ZrQH143K3xgLzxd6SuWqG5Zp1iUmyGgSsJVhdQNeTzAqBFvXXLZqZzFZqocTx4HD9vUVYU27At5i8q46LmBXXL97fo4H9C3tHm4BnjY', 'xprv9s21ZrQH143K48nfuK14gKJtML7eQzV2dAH1RaqAMj8v2zs79uaavA9UTWMxpBdgbMH2mhJLeKGq8AFA6GDnFyWP4rLmknqZAfgFFV718vo', 'xprv9s21ZrQH143K44Bb9G3EVNmLfAUKjTBAA2YtKxF4zc8SLV1o15JBoddhGHE9PGLXePMbEsSjCCvTvP3fUv6yMXZrnHigBboRBn2DmNoJkJg', 'xprv9s21ZrQH143K48PpVxrh71KdViTFhAaiDSVtNFkmbWNYjwwwPbTrcqoVXsgBfue3Gq9b71hQeEbk67JgtTBcpYgKLF8pTwVnGz56f1BaCYt', 'xprv9s21ZrQH143K3pgRcRBRnmcxNkNNLmJrpneMkEXY6o5TWBuJLMfdRpAWdb2cG3yxbL4DxfpUnQpjfQUmwPdVrRGoDJmtAf5u8cyqKCoDV97', 'xprv9s21ZrQH143K3nvcmdjDDDZbDJHpfWZCUiunwraZdcamYcafHvUnZfV51fivH9FPyfo12NyKH5JDxGLsQePyWKtTiJx3pkEaiwxsMLkVapp', 'xprv9s21ZrQH143K2uYgqtYtphEQkFAgiWSqahFUWjgCdKykJagiNDz6Lf7xRVQdtZ7MvkhX9V3pEcK3xTAWZ6Y6ecJqrXnCpzrH9GSHn8wyrT5', 'xprv9s21ZrQH143K2wcRMP75tAEL5JnUx4xU2AbUBQzVVUDP7DHZJkjF3kaRE7tcnPLLLL9PGjYTWTJmCQPaQ4GGzgWEUFJ6snwJG9YnQHBFRNR' ]; console.log('var copayers = ['); _.each(xPrivKeys, function(xPrivKeyStr, c) { var xpriv = Bitcore.HDPrivateKey(xPrivKeyStr); var xpub = Bitcore.HDPublicKey(xpriv); var xpriv_45H = xpriv.deriveChild(45, true); var xpub_45H = Bitcore.HDPublicKey(xpriv_45H); var id45 = Model.Copayer._xPubToCopayerId('btc', xpub_45H.toString()); var xpriv_44H_0H_0H = xpriv.deriveChild(44, true).deriveChild(0, true).deriveChild(0, true); var xpub_44H_0H_0H = Bitcore.HDPublicKey(xpriv_44H_0H_0H); var id44btc = Model.Copayer._xPubToCopayerId('btc', xpub_44H_0H_0H.toString()); var id44bch = Model.Copayer._xPubToCopayerId('bch', xpub_44H_0H_0H.toString()); var xpriv_1H = xpriv.deriveChild(1, true); var xpub_1H = Bitcore.HDPublicKey(xpriv_1H); var priv = xpriv_1H.deriveChild(0).privateKey; var pub = xpub_1H.deriveChild(0).publicKey; console.log('{id44btc: ', "'" + id44btc + "',"); console.log('id44bch: ', "'" + id44bch + "',"); 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; }; /* ETH wallet use the provided key here, probably 44'/0'/0' */ 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, singleAddress: !!opts.singleAddress, coin: opts.coin || 'btc', network: opts.network || 'livenet', nativeCashAddr: opts.nativeCashAddr, useNativeSegwit: opts.useNativeSegwit, }; if (_.isBoolean(opts.supportBIP44AndP2PKH)) walletOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; server.createWallet(walletOpts, function(err, walletId) { if (err) throw err; async.eachSeries(_.range(n), function(i, cb) { var copayerData = TestData.copayers[i + offset]; var pub = (_.isBoolean(opts.supportBIP44AndP2PKH) && !opts.supportBIP44AndP2PKH) ? copayerData.xPubKey_45H : copayerData.xPubKey_44H_0H_0H; if (opts.network == 'testnet') { if (opts.coin == 'btc' || opts.coin == 'bch') { pub = copayerData.xPubKey_44H_0H_0Ht; } else { pub = copayerData.xPubKey_44H_0H_0HtSAME; } } var copayerOpts = helpers.getSignedCopayerOpts({ walletId: walletId, coin: opts.coin, name: 'copayer ' + (i + 1), xPubKey: pub, requestPubKey: copayerData.pubKey_1H_0, customData: 'custom data ' + (i + 1), }); if (_.isBoolean(opts.supportBIP44AndP2PKH)) copayerOpts.supportBIP44AndP2PKH = opts.supportBIP44AndP2PKH; server.joinWallet(copayerOpts, function(err, result) { if (err) throw 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) { if (opts.earlyRet) return cb(s); s.getWallet({}, function(err, w) { // STUB for checkWalletSync. s.checkWalletSync = function(a,b, simple, cb) { if (simple) return cb(null, false); return cb(null, true); } cb(s, w); }); }); }); }); }; helpers.randomTXID = function() { return Bitcore.crypto.Hash.sha256(Buffer.from((Math.random() * 100000).toString())).toString('hex');; }; helpers.toSatoshi = function(btc) { if (_.isArray(btc)) { return _.map(btc, helpers.toSatoshi); } else { return Utils.strip(btc * 1e8); } }; helpers._parseAmount = function(str) { var result = { amount: +0, confirmations: _.random(6, 100), }; if (_.isNumber(str)) str = str.toString(); var re = /^((?:\d+c)|u)?\s*([\d\.]+)\s*(btc|bit|sat)?$/; var 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 (_.endsWith(match[1], 'c')) result.confirmations = +match[1].slice(0, -1); } switch (match[3]) { default: case 'btc': result.amount = Utils.strip(+match[2] * 1e8); break; case 'bit': result.amount = Utils.strip(+match[2] * 1e2); break case 'sat': result.amount = Utils.strip(+match[2]); break; }; return result; }; helpers.stubUtxos = function(server, wallet, amounts, opts, cb) { if (_.isFunction(opts)) { cb = opts; opts = {}; } opts = opts || {}; if (opts.tokenAddress) { amounts = _.isArray(amounts) ? amounts : [amounts]; blockchainExplorer.getBalance = function(opts, cb) { if (opts.tokenAddress) { return cb(null, {unconfirmed:0, confirmed: 2e6, balance: 2e6 }); } let conf = _.sum(_.map(amounts, x => Number((x*1e18).toFixed(0)))); return cb(null, {unconfirmed:0, confirmed: conf, balance: conf }); } blockchainExplorer.estimateFee = sinon.stub().callsArgWith(1, null, 20000000000); return cb(); } if (wallet.coin == 'eth') { amounts = _.isArray(amounts) ? amounts : [amounts]; let conf = _.sum(_.map(amounts, x => Number((x*1e18).toFixed(0)))); blockchainExplorer.getBalance = sinon.stub().callsArgWith(1, null, {unconfirmed:0, confirmed: conf, balance: conf }); return cb(); } if (wallet.coin == 'xrp') { amounts = _.isArray(amounts) ? amounts : [amounts]; let conf = _.sum(_.map(amounts, x => Number((x*1e6).toFixed(0)))); conf = conf + Defaults.MIN_XRP_BALANCE; blockchainExplorer.getBalance = sinon.stub().callsArgWith(1, null, {unconfirmed:0, confirmed: conf, balance: conf }); return cb(); } if (!helpers._utxos) helpers._utxos = {}; var S = Bitcore_[wallet.coin].Script; async.waterfall([ function(next) { if (opts.addresses) return next(null, [].concat(opts.addresses)); async.mapSeries(_.range(0, amounts.length > 2 ? 2 : 1), function(i, next) { server.createAddress({}, next); }, next); }, function(addresses, next) { addresses.should.not.be.empty; var utxos = _.compact(_.map([].concat(amounts), function(amount, i) { var parsed = helpers._parseAmount(amount); if (parsed.amount <= 0) return null; var address = addresses[i % addresses.length]; var scriptPubKey; switch (wallet.addressType) { case Constants.SCRIPT_TYPES.P2SH: scriptPubKey = S.buildMultisigOut(address.publicKeys, wallet.m).toScriptHashOut(); break; case Constants.SCRIPT_TYPES.P2PKH: scriptPubKey = S.buildPublicKeyHashOut(address.address); break; case Constants.SCRIPT_TYPES.P2WPKH: scriptPubKey = S.buildWitnessV0Out(address.address); break; case Constants.SCRIPT_TYPES.P2WSH: scriptPubKey = S.buildWitnessV0Out(address.address); break; } should.exist(scriptPubKey, 'unknown address type:' + wallet.addressType); return { txid: helpers.randomTXID(), vout: _.random(0, 10), satoshis: parsed.amount, scriptPubKey: scriptPubKey.toBuffer().toString('hex'), address: address.address, confirmations: parsed.confirmations, publicKeys: address.publicKeys, wallet: wallet.id, }; })); if (opts.keepUtxos) { helpers._utxos = helpers._utxos.concat(utxos); } else { helpers._utxos = utxos; } blockchainExplorer.getUtxos = function(param1, height, cb) { var selected; selected = _.filter(helpers._utxos, {'wallet': param1.id}); return cb(null, selected); }; blockchainExplorer.getAddressUtxos = function(param1, height, cb) { var selected; selected = _.filter(helpers._utxos, {'address': param1}); return cb(null, selected); }; return next(); }, ], function(err) { should.not.exist(err); return cb(helpers._utxos); }); }; helpers.stubBroadcast = function(txid) { blockchainExplorer.broadcast = sinon.stub().callsArgWith(1, null, txid || '112233'); blockchainExplorer.getTransaction = sinon.stub().callsArgWith(1, null, null); }; helpers.createTxsV8 = function(nr, bcHeight, txs) { txs = txs || []; // Will generate // order / confirmations / height / txid // 0. => -1 / -1 / txid0 / id0 <= LAST ONE! // 1. => 1 / bcHeight / txid1 // 2. => 2 / bcHeight - 1 / txid2 // 3. => 3... / bcHeight - 2 / txid3 var i = 0; if (_.isEmpty(txs)) { while(i < nr) { txs.push({ id: 'id' + i, txid: 'txid' + i, size: 226, category: 'receive', satoshis: 30001, // this is translated on V8.prototype.getTransactions amount: 30001 /1e8, height: (i == 0) ? -1 : bcHeight - i + 1, address: 'muFJi3ZPfR5nhxyD7dfpx2nYZA8Wmwzgck', blockTime: '2018-09-21T18:08:31.000Z', }); i++; } } return txs; }; helpers.stubHistory = function(nr, bcHeight, txs) { txs= helpers.createTxsV8(nr,bcHeight, txs); blockchainExplorer.getTransactions = function(walletId, startBlock, cb) { startBlock = startBlock || 0; var page = _.filter(txs, (x) => { return x.height >=startBlock || x.height == -1 }); return cb(null, page); }; }; helpers.stubCheckData = function(bc, server, isBCH, cb) { server.storage.walletCheck({walletId:server.walletId, bch: isBCH}).then((x) => { bc.getCheckData = sinon.stub().callsArgWith(1, null, {sum: x.sum}); return cb(); }); }; // fill => fill intermediary levels helpers.stubFeeLevels = function(levels, fill, coin) { coin = coin || 'btc'; let div = 1; if (coin == 'btc' || coin == 'bch' || coin == 'doge' || coin == 'ltc') { div = 1e8; // bitcoind returns values in BTC amounts } blockchainExplorer.estimateFee = function(nbBlocks, cb) { var result = _.fromPairs(_.map(_.pick(levels, nbBlocks), function(fee, n) { return [+n, fee > 0 ? fee / div : fee]; })); if (fill) { let last; _.each(nbBlocks, (n) => { if (result[n]) { last = result[n]; } result[n] = last; }); } return cb(null, result); }; }; var stubAddressActivityFailsOn = null; var stubAddressActivityFailsOnCount=1; helpers.stubAddressActivity = function(activeAddresses, failsOn) { stubAddressActivityFailsOnCount=1; // could be null stubAddressActivityFailsOn = failsOn; blockchainExplorer.getAddressActivity = function(address, cb) { if (stubAddressActivityFailsOnCount === stubAddressActivityFailsOn) return cb('failed on request'); stubAddressActivityFailsOnCount++; return cb(null, _.includes(activeAddresses, address)); }; }; helpers.clientSign = function(txp, derivedXPrivKey) { var self = this; //Derive proper key to sign, for each input var privs = []; var derived = {}; var signatures; var xpriv = new Bitcore.HDPrivateKey(derivedXPrivKey, txp.network); switch(txp.coin) { case 'eth': case 'xrp': // For eth => account, 0, change = 0 const priv = xpriv.derive('m/0/0').privateKey; const privKey = priv.toString('hex'); let tx = ChainService.getBitcoreTx(txp).uncheckedSerialize(); const isERC20 = txp.tokenAddress && !txp.payProUrl; const chain = isERC20 ? 'ERC20' : ChainService.getChain(txp.coin); tx = typeof tx === 'string'? [tx] : tx; signatures = []; for (const rawTx of tx) { const signed = CWC.Transactions.getSignature({ chain, tx: rawTx, key: { privKey: privKey.toString('hex') }, }); signatures.push(signed); } break; default: _.each(txp.inputs, function(i) { if (!derived[i.path]) { derived[i.path] = xpriv.deriveChild(i.path).privateKey; privs.push(derived[i.path]); } }); var t = ChainService.getBitcoreTx(txp); signatures = _.map(privs, function(priv, i) { return t.getSignatures(priv, undefined, txp.signingMethod); }); signatures = _.map(_.sortBy(_.flatten(signatures), 'inputIndex'), function(s) { return s.signature.toDER(txp.signingMethod).toString('hex'); }); }; return signatures; }; helpers.getProposalSignatureOpts = function(txp, signingKey) { var raw = txp.getRawTx(); var proposalSignature = helpers.signMessage(raw, signingKey); return { txProposalId: txp.id, proposalSignature: proposalSignature, } }; helpers.createAddresses = function(server, wallet, main, change, cb) { // var clock = sinon.useFakeTimers('Date'); async.mapSeries(_.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) { should.not.exist(err); // clock.restore(); return cb(_.take(addresses, main), _.takeRight(addresses, change)); }); }; helpers.createAndPublishTx = function(server, txOpts, signingKey, cb) { server.createTx(txOpts, function(err, txp) { if (err) console.log(err); should.not.exist(err, "Error creating a TX"); should.exist(txp,"Error... no txp"); var publishOpts = helpers.getProposalSignatureOpts(txp, signingKey); server.publishTx(publishOpts, function(err) { if (err) console.log(err); should.not.exist(err); return cb(txp); }); }); }; helpers.historyCacheTest = function(items) { var template = { txid: "fad88682ccd2ff34cac6f7355fe9ecd8addd9ef167e3788455972010e0d9d0de", vin: [{ txid: "0279ef7b21630f859deb723e28beac9e7011660bd1346c2da40321d2f7e34f04", vout: 0, n: 0, addr: "2NAVFnsHqy5JvqDJydbHPx393LFqFFBQ89V", valueSat: 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 }; var ret = []; _.each(_.range(0, items), function(i) { var t = _.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;