'use strict'; var _ = require('lodash'); var async = require('async'); var chai = require('chai'); var sinon = require('sinon'); var should = chai.should(); var levelup = require('levelup'); var memdown = require('memdown'); var Bitcore = require('bitcore'); var SignUtils = require('../lib/signutils'); var Storage = require('../lib/storage'); var Wallet = require('../lib/model/wallet'); var Address = require('../lib/model/address'); var Copayer = require('../lib/model/copayer'); var CopayServer = require('../lib/server'); var keyPair = { priv: '0dea92f1df6675085b5cdd965487bb862f84f2755bcb56fa45dbf5b387a6c4a0', pub: '026092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80', }; var aPubKey = '042F65F56A6C06C2B651C473AC221B2460DA57859AFB72564E9781B655EBC0AFAF322B9A732324ECC92A3319DFB1F0D53F0CB7E6620C98BD1EF53106A7CF3F6DB9'; var aXPubKey = 'xpub661MyMwAqRbcFHFFvUP6HaKdd2FYzNcZCGagxMzQEf1J3x2DeASBW2JWox7ToGwPM7V2yRzQAxcD6MdPid9C8kwhKkVWBxQ3dMo8zu3pub7'; var aXPubKeySignature = '3045022100f988737147894bbfdc196c1289e4d970b391c0d8e9d1fcc0397f16e6a31c9df2022014d9af9aceccb540f4a5a2680e2aebb1f3df55bcf3778599b78314a02064c592'; // with keyPair.priv // Copayers var someXPrivKey = [ 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e', ]; var someXPubKeys = [ 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', 'xpub661MyMwAqRbcGpExxHEzAWxBQX3k76NyerSpjqucSXXfTqH6Wq9sUVRwTjpHZHwapDbG16KEB9w9r3LT2jKYqU9xJf1YBAaZFikbUHiV1tg', 'xpub661MyMwAqRbcEvKQnt9ELHHcangXssm174sWr5gNTSmQYsAtvQJNUpLETDTm1vDxwtABvB4SRjGkNMm37NnMerKg4e3ygqmWEr75Fka4dK7', 'xpub661MyMwAqRbcG67ioS7rz3fFg7EDQNLJ9m1etAPwBecZhL5kKAKe4JU5jCTzRcEWp28XCYA1gKh7jyficSr97gcR2pjDL5jbWua1CwTKWV4', ]; var someXPubKeysSignatures = [ '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46', '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62', '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c', '304402203ae5bf7fa8935b8ab2ac33724dbb191356cecb47c8371d2c9389e918a3600918022073b48705306730c8fe4ab22d5f6ed3ca3def27eb6e8c5cc8f53e23c11fa5e5ef', '3045022100eabd2a605403b377a8db9eec57726da0309a7eb385e7e4e5273b9862046f25ef02204d18755a90580a98f45e162ae5d5dc39aa3aa708a0d79433ed259e70a832b49c', '3045022100c282254773c65025054e18a61ee550cbf78b88fc72ef66770050815b62502d9c02206e0df528203c9201c144f865df71f5d2471668f4ed8387979fcee20f6fa121a9', ]; // with keyPair.priv //Copayer signature var aText = 'hello world'; var aTextSignature = '3045022100addd20e5413865d65d561ad2979f2289a40d52594b1f804840babd9a63e4ebbf02204b86285e1fcab02df772e7a1325fc4b511ecad79a8f80a2bd1ad8bfa858ac3d4'; // with someXPrivKey[0].derive('m/1/0')=5c0e043a513032907d181325a8e7990b076c0af15ed13dc5e611cda9bb3ae52a; var helpers = {}; helpers.createAndJoinWallet = function(id, m, n, cb) { var walletOpts = { id: id, name: id + ' wallet', m: m, n: n, pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { if (err) return cb(err); async.each(_.range(1, n + 1), function(i, cb) { var copayerOpts = { walletId: id, id: '' + i, name: 'copayer ' + i, xPubKey: someXPubKeys[i - 1], xPubKeySignature: someXPubKeysSignatures[i - 1], }; server.joinWallet(copayerOpts, function(err) { return cb(err); }); }, function(err) { if (err) return cb(err); server.getWallet({ id: id, includeCopayers: true }, function(err, wallet) { return cb(err, wallet); }); }); }); }; helpers.randomTXID = function() { return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');; }; helpers.createUtxos = function(server, wallet, amounts, cb) { var addresses = []; async.each(amounts, function(a, next) { server.createAddress({ walletId: wallet.id, isChange: false, }, function(err, address) { addresses.push(address); next(err); }); }, function(err) { amounts = [].concat(amounts); var i = 0; return cb(_.map(amounts, function(amount) { return { txid: helpers.randomTXID(), vout: Math.floor((Math.random() * 10) + 1), amount: amount, scriptPubKey: addresses[i].getScriptPubKey(wallet.m).toBuffer().toString('hex'), }; })); }); }; var db, storage; var server; describe('Copay server', function() { beforeEach(function() { db = levelup(memdown, { valueEncoding: 'json' }); storage = new Storage({ db: db }); }); describe('#getWallet', function() { beforeEach(function() { server = new CopayServer({ storage: storage, }); }); it('should get existing wallet', function(done) { var w1 = new Wallet({ id: '123', name: 'my wallet', m: 2, n: 3, pubKey: aPubKey, }); var w2 = new Wallet({ id: '234', name: 'my wallet 2', m: 3, n: 4, pubKey: aPubKey, }); db.batch([{ type: 'put', key: 'wallet-123', value: w1, }, { type: 'put', key: 'wallet-234', value: w2, }]); server.getWallet({ id: '123', includeCopayers: true }, function(err, wallet) { should.not.exist(err); wallet.id.should.equal('123'); wallet.name.should.equal('my wallet'); wallet.status.should.equal('pending'); wallet.copayers.length.should.equal(0); done(); }); }); it('should fail when requesting non-existent wallet', function(done) { var w1 = new Wallet({ id: '123', name: 'my wallet', m: 2, n: 3, pubKey: aPubKey, }); var w2 = new Wallet({ id: '234', name: 'my wallet 2', m: 3, n: 4, pubKey: aPubKey, }); db.batch([{ type: 'put', key: 'wallet-123', value: w1, }, { type: 'put', key: 'wallet-234', value: w2, }]); server.getWallet({ id: '345' }, function(err, wallet) { should.exist(err); err.should.equal('Wallet not found'); done(); }); }); }); describe('#createWallet', function() { beforeEach(function() { server = new CopayServer({ storage: storage, }); }); it('should create and store wallet', function(done) { var opts = { id: '123', name: 'my wallet', m: 2, n: 3, pubKey: aPubKey, }; server.createWallet(opts, function(err) { should.not.exist(err); server.getWallet({ id: '123' }, function(err, wallet) { should.not.exist(err); wallet.id.should.equal('123'); wallet.name.should.equal('my wallet'); done(); }); }); }); it('should fail to recreate existing wallet', function(done) { var opts = { id: '123', name: 'my wallet', m: 2, n: 3, pubKey: aPubKey, }; server.createWallet(opts, function(err) { should.not.exist(err); server.getWallet({ id: '123' }, function(err, wallet) { should.not.exist(err); wallet.id.should.equal('123'); wallet.name.should.equal('my wallet'); server.createWallet(opts, function(err) { should.exist(err); done(); }); }); }); }); it('should fail to create wallet with invalid copayer pairs', function(done) { var invalidPairs = [{ m: 0, n: 0 }, { m: 0, n: 2 }, { m: 2, n: 1 }, { m: 0, n: 10 }, { m: 1, n: 20 }, { m: 10, n: 10 }, ]; var opts = { id: '123', name: 'my wallet', pubKey: aPubKey, }; async.each(invalidPairs, function(pair, cb) { opts.m = pair.m; opts.n = pair.n; server.createWallet(opts, function(err) { should.exist(err); err.should.contain('Invalid m/n combination'); return cb(); }); }, function(err) { done(); }); }); }); describe('#joinWallet', function() { beforeEach(function() { server = new CopayServer({ storage: storage, }); }); it('should join existing wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 2, n: 3, pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '123', id: '999', name: 'me', xPubKey: aXPubKey, xPubKeySignature: aXPubKeySignature, }; server.joinWallet(copayerOpts, function(err) { should.not.exist(err); server.getWallet({ id: '123', includeCopayers: true }, function(err, wallet) { wallet.id.should.equal('123'); wallet.copayers.length.should.equal(1); var copayer = wallet.copayers[0]; copayer.id.should.equal('999'); copayer.name.should.equal('me'); done(); }); }); }); }); it('should fail to join non-existent wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 2, n: 3, pubKey: aPubKey, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '234', id: '999', name: 'me', xPubKey: 'dummy', xPubKeySignature: 'dummy', }; server.joinWallet(copayerOpts, function(err) { should.exist(err); done(); }); }); }); it('should fail to join full wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayer1Opts = { walletId: '123', id: '111', name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: someXPubKeysSignatures[0], }; var copayer2Opts = { walletId: '123', id: '222', name: 'me 2', xPubKey: someXPubKeys[1], xPubKeySignature: someXPubKeysSignatures[1], }; server.joinWallet(copayer1Opts, function(err) { should.not.exist(err); server.getWallet({ id: '123' }, function(err, wallet) { wallet.status.should.equal('complete'); server.joinWallet(copayer2Opts, function(err) { should.exist(err); err.should.equal('Wallet full'); done(); }); }); }); }); }); it('should fail to re-join wallet', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, pubKey: keyPair.pub, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '123', id: '111', name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: someXPubKeysSignatures[0], }; server.joinWallet(copayerOpts, function(err) { should.not.exist(err); server.joinWallet(copayerOpts, function(err) { should.exist(err); err.should.equal('Copayer already in wallet'); done(); }); }); }); }); it('should fail to join with bad formated signature', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, pubKey: aPubKey, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '123', id: '111', name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: 'bad sign', }; server.joinWallet(copayerOpts, function(err) { err.should.contain('Bad request'); done(); }); }); }); it('should fail to join with null signature', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, pubKey: aPubKey, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '123', id: '111', name: 'me', xPubKey: someXPubKeys[0], }; try { server.joinWallet(copayerOpts, function(err) {}); } catch (e) { e.should.contain('xPubKeySignature'); done(); } }); }); it('should fail to join with wrong signature', function(done) { var walletOpts = { id: '123', name: 'my wallet', m: 1, n: 1, pubKey: aPubKey, }; server.createWallet(walletOpts, function(err) { should.not.exist(err); var copayerOpts = { walletId: '123', id: '111', name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: someXPubKeysSignatures[0], }; server.joinWallet(copayerOpts, function(err) { err.should.contain('Bad request'); done(); }); }); }); it('should set pkr and status = complete on last copayer joining (2-3)', function(done) { helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) { server.getWallet({ id: '123' }, function(err, wallet) { should.not.exist(err); wallet.status.should.equal('complete'); wallet.publicKeyRing.length.should.equal(3); done(); }); }); }); }); describe('#verifyMessageSignature', function() { beforeEach(function() { server = new CopayServer({ storage: storage, }); }); it('should successfully verify message signature', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { var opts = { walletId: '123', copayerId: '1', message: aText, signature: aTextSignature, }; server.verifyMessageSignature(opts, function(err, isValid) { should.not.exist(err); isValid.should.equal(true); done(); }); }); }); it('should fail to verify message signature when copayer does not exist', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { var opts = { walletId: '123', copayerId: '999', message: 'hello world', signature: 'dummy', }; server.verifyMessageSignature(opts, function(err, isValid) { err.should.equal('Copayer not found'); done(); }); }); }); }); describe('#createAddress', function() { beforeEach(function() { server = new CopayServer({ storage: storage, }); }); it('should create main address', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { server.createAddress({ walletId: '123', isChange: false, }, function(err, address) { should.not.exist(err); address.should.exist; address.address.should.equal('36JdLEUDa6UwCfMhhkdZ2VFnDrGUoLedsR'); address.path.should.equal('m/2147483647/0/0'); done(); }); }); }); it('should create change address', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { server.createAddress({ walletId: '123', isChange: true, }, function(err, address) { should.not.exist(err); address.should.exist; address.address.should.equal('3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH'); address.path.should.equal('m/2147483647/1/0'); done(); }); }); }); it('should create many addresses on simultaneous requests', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { async.map(_.range(10), function(i, cb) { server.createAddress({ walletId: '123', isChange: false, }, cb); }, function(err, addresses) { addresses.length.should.equal(10); addresses[0].path.should.equal('m/2147483647/0/0'); addresses[9].path.should.equal('m/2147483647/0/9'); // No two identical addresses _.keys(_.groupBy(addresses, 'address')).length.should.equal(10); done(); }); }); }); it('should not create address if unable to store wallet', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { var storeWalletStub = sinon.stub(server.storage, 'storeWallet'); storeWalletStub.yields('dummy error'); server.createAddress({ walletId: '123', isChange: true, }, function(err, address) { err.should.exist; should.not.exist(address); server.getAddresses({ walletId: '123' }, function (err, addresses) { addresses.length.should.equal(0); server.storage.storeWallet.restore(); server.createAddress({ walletId: '123', isChange: true, }, function(err, address) { should.not.exist(err); address.should.exist; address.address.should.equal('3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH'); address.path.should.equal('m/2147483647/1/0'); done(); }); }); }); }); }); it('should not create address if unable to store addresses', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { var storeAddressStub = sinon.stub(server.storage, 'storeAddress'); storeAddressStub.yields('dummy error'); server.createAddress({ walletId: '123', isChange: true, }, function(err, address) { err.should.exist; should.not.exist(address); server.getAddresses({ walletId: '123' }, function (err, addresses) { addresses.length.should.equal(0); server.storage.storeAddress.restore(); server.createAddress({ walletId: '123', isChange: true, }, function(err, address) { should.not.exist(err); address.should.exist; address.address.should.equal('3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH'); address.path.should.equal('m/2147483647/1/0'); done(); }); }); }); }); }); }); describe('#createTx', function() { var wallet; beforeEach(function(done) { server = new CopayServer({ storage: storage, }); helpers.createAndJoinWallet('123', 2, 2, function(err, w) { wallet = w; server.createAddress({ walletId: '123', isChange: false, }, function(err, address) { done(); }); }); }); it('should create tx', function(done) { helpers.createUtxos(server, wallet, [100, 200], function(utxos) { var bc = sinon.stub(); bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); server._getBlockExplorer = sinon.stub().returns(bc); var txOpts = { copayerId: '1', walletId: '123', toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 80, message: 'some message', otToken: 'dummy', requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); tx.should.exist; tx.isAccepted().should.equal.false; tx.isRejected().should.equal.false; server.getPendingTxs({ walletId: '123' }, function(err, txs) { should.not.exist(err); txs.length.should.equal(1); server.getBalance({ walletId: '123' }, function(err, balance) { should.not.exist(err); balance.totalAmount.should.equal(300); balance.lockedAmount.should.equal(100); done(); }); }); }); }); }); it('should fail to create tx when insufficient funds', function(done) { helpers.createUtxos(server, wallet, [100], function(utxos) { var bc = sinon.stub(); bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); server._getBlockExplorer = sinon.stub().returns(bc); var txOpts = { copayerId: '1', walletId: '123', toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 120, message: 'some message', otToken: 'dummy', requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { err.should.contain('insufficient'); server.getPendingTxs({ walletId: '123' }, function(err, txs) { should.not.exist(err); txs.length.should.equal(0); server.getBalance({ walletId: '123' }, function(err, balance) { should.not.exist(err); balance.lockedAmount.should.equal(0); balance.totalAmount.should.equal(100); done(); }); }); }); }); }); it('should create tx when there is a pending tx and enough UTXOs', function(done) { helpers.createUtxos(server, wallet, [10.1, 10.2, 10.3], function(utxos) { var bc = sinon.stub(); bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); server._getBlockExplorer = sinon.stub().returns(bc); var txOpts = { copayerId: '1', walletId: '123', toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 12, message: 'some message', otToken: 'dummy', requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); tx.should.exist; var txOpts2 = { copayerId: '1', walletId: '123', toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 8, message: 'some message 2', otToken: 'dummy', requestSignature: 'dummy', }; server.createTx(txOpts2, function(err, tx) { should.not.exist(err); tx.should.exist; server.getPendingTxs({ walletId: '123' }, function(err, txs) { should.not.exist(err); txs.length.should.equal(2); server.getBalance({ walletId: '123' }, function(err, balance) { should.not.exist(err); balance.totalAmount.should.equal(30.6); balance.lockedAmount.should.equal(30.6); done(); }); }); }); }); }); }); it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) { helpers.createUtxos(server, wallet, [10.1, 10.2, 10.3], function(utxos) { var bc = sinon.stub(); bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos); server._getBlockExplorer = sinon.stub().returns(bc); var txOpts = { copayerId: '1', walletId: '123', toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 12, message: 'some message', otToken: 'dummy', requestSignature: 'dummy', }; server.createTx(txOpts, function(err, tx) { should.not.exist(err); tx.should.exist; var txOpts2 = { copayerId: '1', walletId: '123', toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 24, message: 'some message 2', otToken: 'dummy', requestSignature: 'dummy', }; server.createTx(txOpts2, function(err, tx) { err.should.contain('insufficient'); should.not.exist(tx); server.getPendingTxs({ walletId: '123' }, function(err, txs) { should.not.exist(err); txs.length.should.equal(1); server.getBalance({ walletId: '123' }, function(err, balance) { should.not.exist(err); balance.totalAmount.should.equal(30.6); balance.lockedAmount.should.equal(20.3); done(); }); }); }); }); }); }); }); });