PHP WebShell

Текущая директория: /opt/bitgo-express/node_modules/bitgo/test

Просмотр файла: wallet.js

  //
// Tests for Wallet
//
// Copyright 2014, BitGo, Inc.  All Rights Reserved.
//

var assert = require('assert');
var should = require('should');
var Q = require('q');

var BitGoJS = require('../src/index');
var common = require('../src/common');
var TestBitGo = require('./lib/test_bitgo');
var TransactionBuilder = require('../src/transactionBuilder');
var unspentData = require('./fixtures/largeunspents.json');
var crypto = require("crypto");
var _ = require('lodash');
var bitcoin = BitGoJS.bitcoin;

Q.longStackTrace = true;

describe('Wallet API', function() {
  var bitgo;
  var wallets;
  var wallet1, wallet2, wallet3, safewallet;

  before(function(done) {
    BitGoJS.setNetwork('testnet');

    bitgo = new TestBitGo();
    bitgo.initializeTestVars();
    wallets = bitgo.wallets();
    bitgo.authenticateTestUser(bitgo.testUserOTP(), function(err, response) {
      if (err) {
        console.log(err);
        throw err;
      }

      // Fetch the first wallet.
      var options = {
        id: TestBitGo.TEST_WALLET1_ADDRESS
      };
      wallets.get(options, function(err, wallet) {
        if (err) {
          throw err;
        }
        wallet1 = wallet;

        // Fetch the second wallet
        var options = {
          id: TestBitGo.TEST_WALLET2_ADDRESS
        };
        wallets.get(options, function(err, wallet) {
          if (err) {
            throw err;
          }
          wallet2 = wallet;

          // Fetch the third wallet
          var options = {
            id: TestBitGo.TEST_WALLET3_ADDRESS
          };
          wallets.get(options, function(err, wallet) {
            wallet3 = wallet;

            // Fetch legacy safe wallet
            var options = {
              id: "2MvfC3e6njdTXqWDfGvNUqDs5kwimfaTGjK"
            };
            wallets.get(options, function(err, wallet) {
              safewallet = wallet;
              done();
            });
          });
        });
      });
    });
  });

  describe('Invite non BitGo user', function() {
    before(function(done) {
      wallets.listInvites({})
      .done(function(success) {
        success.should.have.property('outgoing');
        Q.all(success.outgoing.map(function(out) {
          return wallets.cancelInvite({ walletInviteId: out.id });
        }))
        .then(function() {
          done();
        });
      }, function(err) {
        err.should.equal(null);
      });
    });

    it('arguments', function(done) {
      assert.throws(function() { bitgo.wallets().cancelInvite({}, function() {}); });

      assert.throws(function() { wallet1.createInvite({}, function() {}); });
      assert.throws(function() { wallet1.createInvite({ email: 'tester@bitgo.com' }, function() {}); });
      done();
    });

    it('invite existing user', function(done) {
      wallet1.createInvite({
        email: TestBitGo.TEST_SHARED_KEY_USER,
        permissions: 'admin'
      })
      .done(function(success) {
        success.should.equal(null);
      }, function(err) {
        err.status.should.equal(400);
        done();
      });
    });

    var walletInviteId;
    it('invite non bitgo user', function(done) {
      wallet1.createInvite({
        email: 'notfoundqery@bitgo.com',
        permissions: 'admin'
      })
      .done(function(success) {
        success.should.have.property('invite');
        walletInviteId = success.invite.id;
        done();
      }, function(err) {
        err.should.equal(null);
      });
    });

    it('cancel invite', function(done) {
      wallets.cancelInvite({
        walletInviteId: walletInviteId
      })
      .done(function(success) {
        success.should.have.property('state');
        success.state.should.equal('canceled');
        success.should.have.property('changed');
        success.changed.should.equal(true);
        done();
      }, function(err) {
        err.should.equal(null);
      });
    });

    it('can invite non bitgo user again', function (done) {
      wallet1.createInvite({
        email: 'notfoundqery@bitgo.com',
        permissions: 'admin'
      })
      .done(function(success) {
        success.should.have.property('invite');
        walletInviteId = success.invite.id;
        done();
      }, function(err) {
        err.should.equal(null);
      });
    });
  });

  var walletShareIdWithViewPermissions, walletShareIdWithSpendPermissions, cancelledWalletShareId;
  describe('Share wallet', function() {
    // clean up any outstanding shares before proceeding
    before(function() {
      return bitgo.wallets().listShares({})
      .then(function(result){
        var cancels = result.outgoing.map(function(share) {
          return bitgo.wallets().cancelShare({ walletShareId: share.id });
        });
        return Q.all(cancels);
      });
    });

    it('arguments', function (done) {
      assert.throws(function () { bitgo.getSharingKey({}); });

      assert.throws(function () { wallet1.shareWallet({}, function() {}); });
      assert.throws(function () { wallet1.shareWallet({ email:'tester@bitgo.com' }, function() {}); });
      // assert.throws(function () { wallet1.shareWallet({ email:'notfoundqery@bitgo.com', walletPassphrase:'wrong' }, function() {}); });
      done();
    });

    it('get sharing key of user that does not exist', function(done) {

      bitgo.getSharingKey({ email:'notfoundqery@bitgo.com' })
      .done(
        function(success) {
          success.should.equal(null);
        },
        function(err) {
          err.status.should.equal(404);
          done();
        }
      );
    });

    it('sharing with user that does not exist', function(done) {

      wallet1.shareWallet({
        email:'notfoundqery@bitgo.com',
        permissions: 'admin,spend,view',
        walletPassphrase:'test'
      })
      .done(
      function(success) {
        success.should.equal(null);
      },
      function(err) {
        err.status.should.equal(404);
        done();
      }
      );
    });

    it('trying to share with an incorrect passcode', function(done) {

      bitgo.unlock({ otp: '0000000' })
      .then(function() {
        wallet1.shareWallet({
          email: TestBitGo.TEST_SHARED_KEY_USER,
          permissions: 'admin,spend,view',
          walletPassphrase: 'wrong'
        })
        .done(
        function(success) {
          success.should.equal(null);
        },
        function(err) {
          err.message.should.include('Unable to decrypt user keychain');
          done();
        }
        );
      });
    });

    it('get sharing key for a user', function(done) {
      var keychains = bitgo.keychains();
      var newKey = keychains.create();

      var options = {
        xpub: newKey.xpub
      };

      bitgo.getSharingKey({ email: TestBitGo.TEST_SHARED_KEY_USER })
      .done(function(result) {

        result.should.have.property('userId');
        result.should.have.property('pubkey');
        result.userId.should.equal(TestBitGo.TEST_SHARED_KEY_USERID);
        done();
      })
    });

    it('share a wallet (view)', function(done) {
      bitgo.unlock({ otp: '0000000' })
      .then(function() {
        return wallet1.shareWallet({
          email: TestBitGo.TEST_SHARED_KEY_USER,
          walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE,
          reshare: true, // for tests, we have actually already shared the wallet, and thus must set reshare
          permissions: 'view'
        });
      })
      .then(function(result){
        result.should.have.property('walletId');
        result.should.have.property('fromUser');
        result.should.have.property('toUser');
        result.should.have.property('state');
        result.walletId.should.equal(wallet1.id());
        result.fromUser.should.equal(TestBitGo.TEST_USERID);
        result.toUser.should.equal(TestBitGo.TEST_SHARED_KEY_USERID);
        result.state.should.equal('active');

        result.should.have.property('id');
        walletShareIdWithViewPermissions = result.id;
        done();
      })
      .done();
    });

    it('remove user from wallet', function() {
      return bitgo.unlock({ otp: '0000000' })
      .then(function() {
        return wallet2.removeUser({
          user: TestBitGo.TEST_SHARED_KEY_USERID
        });
      })
      .then(function(wallet){
        wallet.adminCount.should.eql(1);
        wallet.admin.users.length.should.eql(1);
      });
    });

    it('share a wallet (spend)', function(done) {
      bitgo.unlock({ otp: '0000000' })
      .then(function() {
        return wallet2.shareWallet({
          email: TestBitGo.TEST_SHARED_KEY_USER,
          walletPassphrase: TestBitGo.TEST_WALLET2_PASSCODE,
          permissions: 'view,spend'
        });
      })
      .then(function(result){
        result.should.have.property('walletId');
        result.should.have.property('fromUser');
        result.should.have.property('toUser');
        result.should.have.property('state');
        result.walletId.should.equal(wallet2.id());
        result.fromUser.should.equal(TestBitGo.TEST_USERID);
        result.toUser.should.equal(TestBitGo.TEST_SHARED_KEY_USERID);
        result.state.should.equal('active');

        result.should.have.property('id');
        walletShareIdWithSpendPermissions = result.id;
        done();
      })
      .done();
    });

    it('share a wallet and then cancel the share', function(done) {
      bitgo.unlock({ otp: '0000000' })
      .then(function() {
        return wallet3.shareWallet({
          email: TestBitGo.TEST_SHARED_KEY_USER,
          walletPassphrase: TestBitGo.TEST_WALLET3_PASSCODE,
          permissions: 'view'
        });
      })
      .then(function(result){
        result.should.have.property('walletId');
        result.should.have.property('fromUser');
        result.should.have.property('toUser');
        result.should.have.property('state');
        result.walletId.should.equal(wallet3.id());
        cancelledWalletShareId = result.id;

        return bitgo.wallets().cancelShare({ walletShareId: cancelledWalletShareId }, function(err, result) {

          result.should.have.property('state');
          result.should.have.property('changed');
          result.state.should.equal('canceled');
          result.changed.should.equal(true);
          done();
        });
      })
      .done();
    });
  });

  var bitgoSharedKeyUser;
  describe('Get wallet share list', function() {
    before(function(done) {
      bitgoSharedKeyUser = new TestBitGo();
      bitgoSharedKeyUser.initializeTestVars();
      bitgoSharedKeyUser.authenticate({ username: TestBitGo.TEST_SHARED_KEY_USER, password: TestBitGo.TEST_SHARED_KEY_PASSWORD, otp: '0000000' })
      .then(function(success) {
        done();
      })
      .done();
    });

    it('cancelled wallet share should not be in sender list', function(done) {

      bitgo.wallets().listShares({})
      .then(function(result){
        result.outgoing.should.not.containDeep([{id: cancelledWalletShareId}]);
        done();
      })
      .done();
    });

    it('wallet share should be in sender list', function(done) {
      bitgo.wallets().listShares({})
      .then(function(result){
        result.outgoing.should.containDeep([{id: walletShareIdWithViewPermissions}]);
        result.outgoing.should.containDeep([{id: walletShareIdWithSpendPermissions}]);
        done();
      })
      .done();
    });

    it('wallet share should be in receiver list', function(done) {
      bitgoSharedKeyUser.wallets().listShares({})
      .then(function(result){
        result.incoming.should.containDeep([{id: walletShareIdWithViewPermissions}]);
        result.incoming.should.containDeep([{id: walletShareIdWithSpendPermissions}]);
        done();
      })
      .done();
    });
  });

  describe('Accept wallet share', function (){
    before(function(done) {
      bitgoSharedKeyUser = new TestBitGo();
      bitgoSharedKeyUser.initializeTestVars();
      bitgoSharedKeyUser.authenticate({ username: TestBitGo.TEST_SHARED_KEY_USER, password: TestBitGo.TEST_SHARED_KEY_PASSWORD, otp: '0000000' })
      .then(function(success) {
        done();
      })
      .done();
    });

    it('accept a wallet share with only view permissions', function(done) {
      bitgoSharedKeyUser.wallets().acceptShare({walletShareId: walletShareIdWithViewPermissions})
      .then(function(result) {
        result.should.have.property('state');
        result.should.have.property('changed');
        result.state.should.equal('accepted');
        result.changed.should.equal(true);

        // now check that the wallet share id is no longer there
        return bitgoSharedKeyUser.wallets().listShares({})
      })
      .then(function(result) {
        result.incoming.should.not.containDeep([{id: walletShareIdWithViewPermissions}]);
        done();
      })
      .done();
    });

    it('accept a wallet share with spend permissions', function(done) {
      bitgoSharedKeyUser.unlock({'otp': '0000000'})
      .then(function() {
        return bitgoSharedKeyUser.wallets().acceptShare({walletShareId: walletShareIdWithSpendPermissions, userPassword: TestBitGo.TEST_SHARED_KEY_PASSWORD})
        .then(function (result) {
          result.should.have.property('state');
          result.should.have.property('changed');
          result.state.should.equal('accepted');
          result.changed.should.equal(true);

          // now check that the wallet share id is no longer there
          return bitgoSharedKeyUser.wallets().listShares()
        })
        .then(function (result) {
          result.incoming.should.not.containDeep([{id: walletShareIdWithSpendPermissions}]);
          done();
        })
        .done();
      })
      .done();
    });
  });

  describe('Wallet shares with skip keychain', function () {

    var walletShareId;
    it('share a wallet (spend) without keychain', function(done) {
      bitgo.unlock({ otp: '0000000' })
      .then(function() {
        return wallet2.shareWallet({
          email: TestBitGo.TEST_SHARED_KEY_USER,
          skipKeychain: true,
          reshare: true, // for tests, we have actually already shared the wallet, and thus must set reshare
          permissions: 'view,spend'
        });
      })
      .then(function(result){
        result.should.have.property('walletId');
        result.should.have.property('fromUser');
        result.should.have.property('toUser');
        result.should.have.property('state');
        result.walletId.should.equal(wallet2.id());
        result.fromUser.should.equal(TestBitGo.TEST_USERID);
        result.toUser.should.equal(TestBitGo.TEST_SHARED_KEY_USERID);
        result.state.should.equal('active');

        result.should.have.property('id');
        walletShareId = result.id;
        done();
      })
      .done();
    });

    it('accept a wallet share without password', function(done) {
      bitgoSharedKeyUser.unlock({'otp': '0000000'})
      .then(function() {
        return bitgoSharedKeyUser.wallets().acceptShare({ walletShareId: walletShareId, overrideEncryptedXprv: 'test' })
        .then(function (result) {
          result.should.have.property('state');
          result.should.have.property('changed');
          result.state.should.equal('accepted');
          result.changed.should.equal(true);

          // now check that the wallet share id is no longer there
          return bitgoSharedKeyUser.wallets().listShares()
        })
        .then(function (result) {
          result.incoming.should.not.containDeep([{id: walletShareId}]);
          done();
        })
        .done();
      })
      .done();
    });
  });

  describe('CreateAddress', function() {
    var addr;

    it('arguments', function(done) {
      assert.throws(function() { wallet2.createAddress('invalid', function() {}); });
      assert.throws(function() { wallet2.createAddress({}, 'invalid'); });
      done();
    });

    it('create', function(done) {
      wallet2.createAddress({}, function(err, address) {
        assert.equal(err, null);
        address.should.have.property('path');
        address.should.have.property('redeemScript');
        address.should.have.property('address');
        addr = address;
        assert.notEqual(address.address, wallet2.id());

        // TODO: Verify the chain?
        done();
      });
    });

    it('validate address', function() {
      assert.throws(function() {
        wallet2.validateAddress({address: addr.address, path: '0/0'});
      });
      assert.throws(function() {
        wallet2.validateAddress({address: addr.address, path: '/0/0'});
      });
      wallet2.validateAddress(addr);
      wallet2.validateAddress({ address: TestBitGo.TEST_WALLET2_ADDRESS, path: '/0/0' });
    });
  });

  describe('GetAddresses', function() {
    it('arguments', function(done) {
      assert.throws(function() { wallet1.addresses('invalid', function() {}); });
      assert.throws(function() { wallet1.addresses({}, 'invalid'); });
      done();
    });

    it('get', function(done) {
      var options = { };
      wallet1.addresses(options, function(err, addresses) {
        assert.equal(err, null);
        addresses.should.have.property('addresses');
        addresses.should.have.property('start');
        addresses.should.have.property('count');
        addresses.should.have.property('total');
        var firstAddress = addresses.addresses[0];
        firstAddress.should.have.property('chain');
        firstAddress.should.have.property('index');
        firstAddress.should.have.property('path');

        assert.equal(Array.isArray(addresses.addresses), true);
        assert.equal(addresses.addresses.length, addresses.count);
        done();
      });
    });

    it('getWithLimit1', function(done) {
      var options = { limit: 1 };
      wallet1.addresses(options, function(err, addresses) {
        assert.equal(err, null);
        addresses.should.have.property('addresses');
        addresses.should.have.property('start');
        addresses.should.have.property('count');
        addresses.should.have.property('total');
        var firstAddress = addresses.addresses[0];
        firstAddress.should.have.property('chain');
        firstAddress.should.have.property('index');
        firstAddress.should.have.property('path');

        assert.equal(Array.isArray(addresses.addresses), true);
        assert.equal(addresses.addresses.length, addresses.count);
        assert.equal(addresses.addresses.length, 1);
        done();
      });
    });
  });

  describe('GetAddress', function() {
    it('arguments', function(done) {
      assert.throws(function() { wallet1.address('invalid', function() {}); });
      assert.throws(function() { wallet1.address({}, 'invalid'); });
      done();
    });

    it('get', function() {
      var options = { address: wallet1.id() };
      return wallet1.address(options)
      .then(function(result) {
        result.address.should.eql(wallet1.id());
        result.chain.should.eql(0);
        result.index.should.eql(0);
        result.redeemScript.should.not.eql("");
        result.sent.should.be.greaterThan(0);
        result.received.should.be.greaterThan(0);
        result.txCount.should.be.greaterThan(0);
        result.balance.should.be.greaterThan(0);
      });
    });
  });

  describe('Labels', function() {
    it('list', function(done) {
      // delete all labels from wallet1
      wallet1.labels({}, function(err, labels) {
        if (labels == null) {
          return;
        }

        labels.forEach (function(label) {
          wallet1.deleteLabel({address: label.address}, function(err, label) {
            assert.equal(err, null);
          });
        });
      });

      // create a single label on TestBitGo.TEST_WALLET1_ADDRESS2 and check that it is returned
      wallet1.setLabel({label: "testLabel", address: TestBitGo.TEST_WALLET1_ADDRESS2}, function(err, label) {
        // create a label on wallet2's TEST_WALLET2_ADDRESS to ensure that it is not returned
        wallet2.setLabel({label: "wallet2TestLabel", address: TestBitGo.TEST_WALLET2_ADDRESS}, function(err, label2) {
          wallet1.labels({}, function(err, labels) {
            assert.equal(err, null);
            labels.forEach (function(label) {
              label.should.have.property('label');
              label.should.have.property('address');
              label.label.should.eql("testLabel");
              label.address.should.eql(TestBitGo.TEST_WALLET1_ADDRESS2);
            });
            done();
          });
        });
      });
    });
  });

  describe('SetLabel', function() {

    it('arguments', function(done) {
      assert.throws(function() { wallet1.setLabel({}, function() {}); });
      assert.throws(function() { wallet1.setLabel({label: "testLabel"}, function() {}); });
      assert.throws(function() { wallet1.setLabel({address: TestBitGo.TEST_WALLET1_ADDRESS2}, function() {}); });
      assert.throws(function() { wallet1.setLabel({label: "testLabel", address: "invalidAddress"}, function() {}); });
      assert.throws(function() { wallet1.setLabel({label: "testLabel", address: TestBitGo.TEST_WALLET2_ADDRESS2}, function() {}); });
      done();
    });

    it('create', function(done) {
      wallet1.setLabel({label: "testLabel", address: TestBitGo.TEST_WALLET1_ADDRESS2}, function(err, label) {
        assert.equal(err, null);
        label.should.have.property('label');
        label.should.have.property('address');
        label.label.should.eql("testLabel");
        label.address.should.eql(TestBitGo.TEST_WALLET1_ADDRESS2);
        done();
      });
    });
  });

  describe('Rename Wallet / Set Wallet Label', function() {

    it('arguments', function(done) {
      assert.throws(function() { wallet1.setLabel({}, function() {}); });
      done();
    });

    it('should rename wallet', function() {
      // generate some random string to make the rename visible in the system
      var renameIndicator = crypto.randomBytes(3).toString('hex');
      var originalWalletName = 'Even Better Test Wallet 1';
      var newWalletName = originalWalletName + '(' + renameIndicator + ')';
      return wallet1.setWalletName({ label: newWalletName })
      .then(function(result){
        result.should.have.property('id');
        result.should.have.property('label');
        result.id.should.eql(TestBitGo.TEST_WALLET1_ADDRESS);
        result.label.should.eql(newWalletName);

        // now, let's rename it back
        return wallet1.setWalletName({ label: originalWalletName });
      })
      .catch(function(err){
        // it should never be in here
        assert.equal(err, null);
      });
    });
  });

  describe('DeleteLabel', function() {

    it('arguments', function(done) {
      assert.throws(function() { wallet1.deleteLabel({}, function() {}); });
      assert.throws(function() { wallet1.deleteLabel({address: "invalidAddress"}, function() {}); });
      done();
    });

    it('delete', function(done) {
      wallet1.deleteLabel({address: TestBitGo.TEST_WALLET1_ADDRESS2}, function(err, label) {
        assert.equal(err, null);
        label.should.have.property('address');
        label.address.should.eql(TestBitGo.TEST_WALLET1_ADDRESS2);
        done();
      });
    });
  });

  describe('Unspents', function() {

    var sharedWallet;

    before(function() {
      var consolidationBitgo = new TestBitGo();
      consolidationBitgo.initializeTestVars();

      return consolidationBitgo.authenticateTestUser(consolidationBitgo.testUserOTP())
      .then(function() {
        return consolidationBitgo.unlock({ otp: consolidationBitgo.testUserOTP(), duration: 3600 })
      })
      .then(function() {
        return consolidationBitgo.wallets().get({id: TestBitGo.TEST_WALLET2_ADDRESS})
      })
      .then(function(result) {
        sharedWallet = result;
      });
    });

    it('arguments', function(done) {
      assert.throws(function() { wallet1.unspents('invalid', function() {}); });
      assert.throws(function() { wallet1.unspents({target: 'a string!'}, function() {}); });
      assert.throws(function() { wallet1.unspents({}, 'invalid'); });
      assert.throws(function() { wallet1.unspentsPaged('invalid', function() {}); });
      assert.throws(function() { wallet1.unspentsPaged({target: 'a string!'}, function() {}); });
      assert.throws(function() { wallet1.unspentsPaged({limit: 'a string!'}, function() {}); });
      assert.throws(function() { wallet1.unspentsPaged({skip: 'a string!'}, function() {}); });
      assert.throws(function() { wallet1.unspentsPaged({minConfirms: 'a string!'}, function() {}); });
      assert.throws(function() { wallet1.unspentsPaged({}, 'invalid'); });
      done();
    });

    it('list', function(done) {
      var options = { limit: 0.5 * 1e8 };
      wallet1.unspents(options, function(err, unspents) {
        assert.equal(err, null);
        assert.equal(Array.isArray(unspents), true);
        done();
      });
    });

    it('list with minconfirms', function(done) {
      var options = { minConfirms: 5 };
      wallet1.unspents(options, function(err, unspents) {
        _.forEach(unspents, function(unspent) {
          unspent.confirmations.should.be.greaterThan(4);
        });
        done();
      });
    });

    it('list instant only', function(done) {
      var options = { target: 500 * 1e8, instant: true };
      wallet3.unspents(options, function(err, unspents) {
        _.every(unspents, function(unspent) {
          return unspent.instant === true;
        }).should.eql(true);
        done();
      });
    });

    it('list paged', function() {
      var options = { minConfirms: 1, limit: 3 };
      return wallet3.unspentsPaged(options)
      .then(function(result) {
        result.should.have.property('start');
        result.should.have.property('count');
        result.should.have.property('total');
        result.should.have.property('unspents');
        result.unspents.length.should.eql(result.count);
        result.start.should.eql(0);
        result.count.should.eql(3);
      });
    });

    it('list paged with target', function() {
      var options = { target: 50 * 1e8 };
      return wallet1.unspentsPaged(options)
      .then(function(result) {
        result.should.have.property('count');
        result.should.have.property('total');
        result.should.have.property('unspents');
      });
    });

    describe('Unspent Fanning And Consolidation', function(){

      var regroupWallet;
      before(function() {
        var walletParams = {
          id: TestBitGo.TEST_WALLET_REGROUP_ADDRESS
        };
        return bitgo.wallets().get(walletParams)
        .then(function(wallet) {
          regroupWallet = wallet;
        })
      });

      it('arguments', function(done){
        assert.throws(function() { regroupWallet.fanOutUnspents('invalid'); });
        assert.throws(function() { regroupWallet.fanOutUnspents({}); });
        assert.throws(function() { regroupWallet.fanOutUnspents({target: -4}); });
        assert.throws(function() { regroupWallet.fanOutUnspents({target: 0}); });
        assert.throws(function() { regroupWallet.fanOutUnspents({target: 2.3}); });

        assert.throws(function() { regroupWallet.consolidateUnspents('invalid'); });
        assert.throws(function() { regroupWallet.consolidateUnspents({target: -4}); });
        assert.throws(function() { regroupWallet.consolidateUnspents({target: 0}); });
        assert.throws(function() { regroupWallet.consolidateUnspents({target: 2.3}); });
        assert.throws(function() { regroupWallet.consolidateUnspents({target: 3, maxInputCountPerConsolidation: -4}); });
        assert.throws(function() { regroupWallet.consolidateUnspents({target: 3, maxInputCountPerConsolidation: 0}); });
        assert.throws(function() { regroupWallet.consolidateUnspents({target: 3, maxInputCountPerConsolidation: -2.3}); });
        done();
      });

      it('prepare unspents', function() {
        var options = {
          walletPassphrase: TestBitGo.TEST_WALLET_REGROUP_PASSCODE,
          otp: '0000000',
          target: 2,
          minConfirms: 0
        };
        // this unit test should simply not throw an error
        return bitgo.unlock({ otp: '0000000' })
        .then(function() {
          return regroupWallet.regroupUnspents(options);
        });
      });

      it('fan out unspents', function() {

        var options = {
          walletPassphrase: TestBitGo.TEST_WALLET_REGROUP_PASSCODE,
          otp: '0000000',
          target: 10, // the maximum consolidation count per input will be 7. This is to ensure we have multiple batches
          validate: false,
          minConfirms: 0
        };

        return Q.delay(5000) // allow time for unspents to be registered
        .then(function() {
          return bitgo.unlock({ otp: '0000000' })
        })
        .then(function() {
          return regroupWallet.fanOutUnspents(options);
        })
        .then(function(response) {
          response.should.have.property('hash');
          response.should.have.property('tx');
          response.status.should.equal('accepted');
        });

      });

      it('consolidate unspents with automatic input count per consolidation', function() {

        var options = {
          walletPassphrase: TestBitGo.TEST_WALLET_REGROUP_PASSCODE,
          otp: '0000000',
          target: 8,
          validate: false,
          minConfirms: 0
        };

        return Q.delay(5000) // allow time for unspents to be registered
        .then(function() {
          return bitgo.unlock({ otp: '0000000' })
        })
        .then(function() {
          return regroupWallet.consolidateUnspents(options);
        })
        .then(function(response) {
          response.length.should.equal(1);
          var firstConsolidation = response[0];
          firstConsolidation.should.have.property('hash');
          firstConsolidation.should.have.property('tx');
          firstConsolidation.status.should.equal('accepted');
        });

      });

      xit('consolidate unspents', function() {
        var maxInputCountPerConsolidation = 3;
        var progressCallbackCount = 0;
        var progressCallback = function(progressDetails){
          progressDetails.should.have.property('index');
          progressDetails.should.have.property('inputCount');
          progressDetails.index.should.equal(progressCallbackCount);
          assert(progressDetails.inputCount <= maxInputCountPerConsolidation);
          progressCallbackCount++;
        };

        var options = {
          walletPassphrase: TestBitGo.TEST_WALLET_REGROUP_PASSCODE,
          otp: '0000000',
          target: 2,
          maxInputCountPerConsolidation: maxInputCountPerConsolidation,
          validate: false,
          minConfirms: 0,
          progressCallback: progressCallback
        };

        return Q.delay(5000)
        .then(function() {
          return bitgo.unlock({ otp: '0000000' })
        })
        .then(function() {
          return regroupWallet.consolidateUnspents(options);
        })
        .then(function(response) {
          response.length.should.equal(1);
          progressCallbackCount.should.equal(3);
          var firstConsolidation = response[0];
          firstConsolidation.should.have.property('hash');
          firstConsolidation.should.have.property('tx');
          firstConsolidation.status.should.equal('accepted');
        });
      });
    });
  });


  describe('Instant', function() {
    it('wallet1 cannot send instant', function() {
      return Q()
      .then(function() {
        return wallet1.canSendInstant();
      })
      .then(function(result) {
        (!!result).should.eql(false);
      });
    });

    it('wallet3 can send instant', function() {
      return Q()
      .then(function() {
        return wallet3.canSendInstant();
      })
      .then(function(result) {
        result.should.eql(true);
      });
    });

    it('wallet1 cannot get instant balance', function() {
      return Q()
      .then(function() {
        return wallet1.instantBalance();
      })
      .catch(function(error) {
        error.message.should.include('not an instant wallet');
      });
    });

    it('wallet3 instant balance', function() {
      var instantBalance = wallet3.instantBalance();
      instantBalance.should.be.greaterThan(-1);
    });
  });

  describe('Transactions', function() {
    it('arguments', function(done) {
      assert.throws(function() { wallet1.transactions('invalid', function() {}); });
      assert.throws(function() { wallet1.transactions({}, 'invalid'); });
      done();
    });

    var txHash0;
    it('list', function(done) {
      var options = { };
      wallet1.transactions(options, function(err, result) {
        assert.equal(err, null);
        assert.equal(Array.isArray(result.transactions), true);
        result.should.have.property('total');
        result.should.have.property('count');
        result.start.should.eql(0);
        txHash0 = result.transactions[0].id;
        done();
      });
    });

    var limitedTxes;
    var limitTestNumTx = 6;
    var totalTxCount;
    it('list with limit', function(done) {

      var options = { limit: limitTestNumTx };
      wallet1.transactions(options, function(err, result) {
        assert.equal(err, null);
        assert.equal(Array.isArray(result.transactions), true);
        result.should.have.property('total');
        result.should.have.property('count');
        result.start.should.eql(0);
        result.count.should.eql(limitTestNumTx);
        result.transactions.length.should.eql(result.count);
        limitedTxes = result.transactions;
        totalTxCount = result.total;
        done();
      });
    });

    it('list with minHeight', function(done) {

      var minHeight = 530000;
      var options = { minHeight: minHeight, limit: 500 };
      wallet1.transactions(options, function(err, result) {
        assert.equal(err, null);
        assert.equal(Array.isArray(result.transactions), true);
        result.should.have.property('total');
        result.should.have.property('count');
        result.start.should.eql(0);
        result.transactions.length.should.eql(result.count);
        result.transactions.forEach(function(transaction) {
          if (!transaction.pending) {
            transaction.height.should.be.above(minHeight - 1);
          }
        });
        result.total.should.be.below(totalTxCount);
        done();
      });
    });


    it('list with limit and skip', function(done) {
      var skipNum = 2;
      var options = { limit: (limitTestNumTx - skipNum), skip: skipNum };
      wallet1.transactions(options, function(err, result) {
        assert.equal(err, null);
        assert.equal(Array.isArray(result.transactions), true);
        result.should.have.property('total');
        result.should.have.property('count');
        result.start.should.eql(skipNum);
        result.count.should.eql(limitTestNumTx - skipNum);
        result.transactions.length.should.eql(result.count);
        limitedTxes = limitedTxes.slice(skipNum);
        result.transactions.should.eql(limitedTxes);
        done();
      });
    });

    it('get transaction', function(done) {
      var options = { id: txHash0 };
      wallet1.getTransaction(options, function(err, result) {
        assert.equal(err, null);
        result.should.have.property('fee');
        result.should.have.property('outputs');
        result.outputs.length.should.not.eql(0);
        result.should.have.property('entries');
        result.entries.length.should.not.eql(0);
        result.should.have.property('confirmations');
        result.should.have.property('hex');
        done();
      });
    });

    it('get transaction with travel info', function() {
      var keychain;
      var options = {
        xpub: wallet1.keychains[0].xpub,
      };
      return bitgo.keychains().get({ xpub: wallet3.keychains[0].xpub })
      .then(function(res) {
        keychain = res;
        res.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET3_PASSCODE, input: keychain.encryptedXprv });
        return wallet3.getTransaction({ id: TestBitGo.TRAVEL_RULE_TXID });
      })
      .then(function(tx) {
        tx.should.have.property('receivedTravelInfo');
        tx.receivedTravelInfo.should.have.length(2);
        tx = bitgo.travelRule().decryptReceivedTravelInfo({ tx: tx, keychain: keychain });
        var infos = tx.receivedTravelInfo;
        infos.should.have.length(2);
        var info = infos[0].travelInfo;
        info.fromUserName.should.equal('Alice');
        info.toEnterprise.should.equal('SDKOther');
        info = infos[1].travelInfo;
        info.fromUserName.should.equal('Bob');
      });
    });
  });

  describe('TransactionBuilder', function() {
    describe('check', function() {
      it('arguments', function() {
        assert.throws(function() { new TransactionBuilder.createTransaction(); });
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: 'should not be a string'}); });
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}}); });
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: 'should not be a string'}); });
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: {}, fee: 'should not be a string'}); });
      });

      it('recipient arguments', function() {
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: { 123: true }}); });
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: { '123': 'should not be a string' }}); });

        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: { 'string': 'should not be a string' }}); });
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: { 'string': 10000 }}); });
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 1e8;
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: [recipients]}); });
      });

      it('minConfirms argument', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 1e8;
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: recipients, fee: 0, minConfirms: 'string'}); });
      });

      it('fee', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 1e8;
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: recipients, fee: 0.5 * 1e8}); });
      });

      it('fee and feerate', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 1e8;
        assert.throws(function() { new TransactionBuilder.createTransaction({wallet: {}, recipients: recipients, fee: 0.5 * 1e8, feeRate: 0.001 * 1e8}); });
      });
    });

    describe('prepare', function() {
      it('insufficient funds', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = wallet1.balance() + 1e8;
        return Q()
        .then(function() {
          return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients });
        })
        .catch(function(e) {
          e.message.should.eql('Insufficient funds');
          e.result.should.have.property('fee');
          e.result.should.have.property('txInfo');
          e.result.txInfo.should.have.property('nP2SHInputs');
          e.result.txInfo.should.have.property('nP2PKHInputs');
          e.result.txInfo.should.have.property('nOutputs');
          e.result.txInfo.nP2PKHInputs.should.eql(0);
        });
      });

      it('insufficient funds on an empty wallet', function() {
        var wallet;
        var options = {
          "passphrase": TestBitGo.TEST_WALLET1_PASSCODE,
          "label": "temp-empty-wallet",
          "backupXpubProvider": "keyvault-io"
        };

        return bitgo.wallets().createWalletWithKeychains({
          "passphrase": TestBitGo.TEST_WALLET1_PASSCODE,
          "label": "temp-empty-wallet-1"
        })
        .then(function(result) {
          result.should.have.property('wallet');
          wallet = result.wallet;
          var recipients = {};
          recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 1e8; // wallet is empty
          return TransactionBuilder.createTransaction({wallet: wallet, recipients: recipients});
        })
        .catch(function(e) {
          e.message.should.eql('Insufficient funds');
          e.result.should.have.property('fee');
          e.result.should.have.property('txInfo');
          e.result.txInfo.should.have.property('nP2SHInputs');
          e.result.txInfo.should.have.property('nP2PKHInputs');
          e.result.txInfo.should.have.property('nOutputs');
          e.result.txInfo.nP2SHInputs.should.eql(0);
          e.result.txInfo.nP2PKHInputs.should.eql(0);

          return wallet.delete({});
        });
      });

      it('conflicting output script and address', function() {
        var recipients = [];
        recipients.push({ address: '2Mx3TZycg4XL5sQFfERBgNmg9Ma7uxowK9y', script: '76a914cd3af9b7b4587133693da3f40854da2b0ac99ec588ad', amount: wallet1.balance() - 5000 });
        return Q()
        .then(function() {
          return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients});
        })
        .then(function() {
          throw new Error('should not be here!!');
        })
        .catch(function(e) {
          e.message.should.include('both script and address provided but they did not match');
        });
      });

      it('insufficient funds due to fees', function() {
        // Attempt to spend the full balance - adding the default fee would be insufficient funds.
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = wallet1.balance();
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients})
        .then(function(res) {
          throw new Error('succeeded');
        })
        .catch(function(e) {
          e.message.should.eql('Insufficient funds');
          e.result.should.have.property('fee');
        })
        .done();
      });

      xit('prepare wallet1 for transaction size estimation', function() {
        var options = {
          walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE,
          otp: '0000000',
          target: 15,
          validate: false,
          minConfirms: 0
        };

        return Q.delay(5000) // allow time for unspents to be registered
        .then(function() {
          return bitgo.unlock({ otp: '0000000' })
        })
        .then(function() {
          return wallet1.consolidateUnspents(options);
        })
        .catch(function(error) {
          // even if there was an error, it was fine. We only want to reduce the number of unspents if it is more than 15
          console.log('wallet 1 not consolidated');
          console.dir(error);
        });
      });

      it('no change required', function() {
        // Attempt to spend the full balance without any fees.

        var walletmock = Object.create(wallet1);

        // prepare the mock
        return wallet1.unspentsPaged(arguments)
        .then(function(unspents) {
          // it's ascending by default, but we need it to be descending
          var sortedUnspents = _.reverse(_.sortBy(unspents.unspents, 'value'));

          // limit the amount to no more than 15 unspents
          var filteredArray = _.take(sortedUnspents, 15);

          unspents.total = filteredArray.length;
          unspents.count = filteredArray.length;
          unspents.unspents = filteredArray;
          walletmock.wallet.balance = _.sumBy(filteredArray, 'value');

          walletmock.unspentsPaged = function() {
            return Q.fcall(function() {
              return unspents;
            });
          };
        })
        .then(function() {
          var recipients = {};
          recipients[TestBitGo.TEST_WALLET2_ADDRESS] = walletmock.balance();
          return TransactionBuilder.createTransaction({
            wallet: walletmock,
            recipients: recipients,
            fee: 0,
            minConfirms: 0,
            bitgoFee: { amount: 0, address: 'foo' }
          })
        })
        .then(function(result) {
          result.fee.should.equal(0);
          result.changeAddresses.length.should.equal(0);
          result.bitgoFee.amount.should.equal(0);
        });
      });

      it('ok', function() {
        var recipients = [];
        recipients.push({ address: 'n3Eii3DYh5z3SMzWiq7ZVS43bQLvuArsd4', script: '76a914ee40c53bd6f0dcc34f024b6dd13803db2bc8beba88ac', amount: 0.01 * 1e8 });
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.01 * 1e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients})
        .then(function(result) {
          result.should.have.property('unspents');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.walletId.should.equal(wallet1.id());
        });
      });
    });

    describe('size calculation and fees', function() {
      var patch;
      var patch2;
      var patch3;
      before(function() {
        // Monkey patch wallet1 with simulated inputs
        patch = wallet1.unspents;
        patch3 = wallet1.unspentsPaged;
        patch4 = wallet1.createAddress;
        wallet1.unspents = function(options, callback) {
          return Q(unspentData.unspents).nodeify(callback);
        };
        wallet1.unspentsPaged = function(options, callback) {
          return Q(unspentData).nodeify(callback);
        };
        wallet1.createAddress = function(options, callback) {
          var changeAddress = { address: '2N1Dk6C74PM5xoUzEdoPLpWEWufULRwSag7',
            chain: 1,
            index: 8838,
            path: '/1/8838',
            redeemScript: '52210369c90fd18fd7d6bd028d02486997f38cd54365780db5f2a046994cd63680truncated'
          };
          return Q(changeAddress).nodeify(callback);
        };
        patch2 = wallet1.bitgo.estimateFee;
        wallet1.bitgo.estimateFee = function(options, callback) {
          var serverFeeRates = {
            1: 0.000138 * 1e8,
            2: 0.000112 * 1e8,
            3: 0.0000312 * 1e8,
            4: 1.9 * 1e8 // fee rate too high, should fallback to 0.0001
          };
          return Q({
            feePerKb: serverFeeRates[options.numBlocks || 2] || 0.0001,
            numBlocks: options.numBlocks
          }).nodeify(callback);
        };
      });

      after(function() {
        wallet1.unspents = patch;
        wallet1.bitgo.estimateFee = patch2;
        wallet1.unspentsPaged = patch3;
        wallet1.createAddress = patch4;
      });

      it('too large for blockchain relay', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 10000 * 1e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients})
        .catch(function(e) {
          e.message.should.include('transaction too large');
        });
      });

      it('estimateFee with amount', function() {
        return wallet1.estimateFee({ amount: 6200 * 1e8, noSplitChange: true })
        .then(function(result) {
          result.feeRate.should.eql(0.000112 * 1e8);
          result.estimatedSize.should.eql(75327);
          result.fee.should.eql(843663);
        });
      });

      it('estimateFee with recipients (1 recipient)', function() {
        var recipients = [];
        recipients.push({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 6200 * 1e8});
        return wallet1.estimateFee({ recipients: recipients, noSplitChange: true })
        .then(function(result) {
          result.feeRate.should.eql(0.000112 * 1e8);
          result.estimatedSize.should.eql(75327);
          result.fee.should.eql(843663);
        });
      });

      it('estimateFee with recipients (2 recipients)', function() {
        var recipients = [];
        recipients.push({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 6195 * 1e8});
        recipients.push({ address: TestBitGo.TEST_WALLET3_ADDRESS, amount: 5 * 1e8});
        return wallet1.estimateFee({ recipients: recipients, noSplitChange: true })
        .then(function(result) {
          result.feeRate.should.eql(0.000112 * 1e8);
          result.estimatedSize.should.eql(75361);
          result.fee.should.eql(844044);
        });
      });

      it('approximate', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, noSplitChange: true })
        .then(function(result) {
          var feeUsed = result.fee;
          // Note that the transaction size here will be fairly small, because the signatures have not
          // been applied.  But we had to estimate our fees already.
          result.feeRate.should.eql(0.000112 * 1e8);
          result.walletId = wallet1.id;
          result.fee.should.eql(843663);
          result.should.have.property('txInfo');
          result.txInfo.nP2SHInputs.should.eql(255);
          result.txInfo.nP2PKHInputs.should.eql(0);
          result.txInfo.nOutputs.should.eql(3);
        });
      });

      it('approximate with double fees', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, fee: undefined, feeRate: 0.0002 * 1e8, noSplitChange: true })
        .then(function(result) {
          var feeUsed = result.fee;
          // Note that the transaction size here will be fairly small, because the signatures have not
          // been applied.  But we had to estimate our fees already.
          assert.equal(feeUsed, 1506540);
        });
      });

      it('do not override', function() {
        var manualFee = 0.04 * 1e8;
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, fee: manualFee})
        .then(function(result) {
          assert.equal(result.fee, manualFee);
        });
      });

      it('approximate with feeRate set by feeTxConfirmTarget 1 (estimatefee monkeypatch)', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, feeTxConfirmTarget: 1, noSplitChange: true })
        .then(function(result) {
          var feeUsed = result.fee;
          assert.equal(feeUsed, 1039513); // tx size will be 75kb * 0.000138 * 1e8
          result.should.have.property('txInfo');
          result.txInfo.nP2SHInputs.should.eql(255);
          result.txInfo.nP2PKHInputs.should.eql(0);
          result.txInfo.nOutputs.should.eql(3);
        });
      });

      it('approximate with feeRate with maxFeeRate (server gives too high a fee and we use max)', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, feeTxConfirmTarget: 1, maxFeeRate: 5000, noSplitChange: true })
        .then(function(result) {
          var feeUsed = result.fee;
          assert.equal(feeUsed, 376635);
        });
      });

      it('approximate with feeRate set by feeTxConfirmTarget 3 (estimatefee monkeypatch)', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, feeTxConfirmTarget: 3, noSplitChange: true })
        .then(function(result) {
          var feeUsed = result.fee;
          assert.equal(feeUsed, 235021); // tx size will be 75kb * 0.0000312 * 1e8
        });
      });

      it('approximate with feeRate with maxFeeRate (real service)', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        // undo the monkey patch so we get the right max fee
        var feeMonkeyPatch = wallet1.bitgo.estimateFee;
        wallet1.bitgo.estimateFee = patch2;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, feeTxConfirmTarget: 3, maxFeeRate: 2200, noSplitChange: true })
        .then(function(result) {
          wallet1.bitgo.estimateFee = feeMonkeyPatch;
          var feeUsed = result.fee;
          assert.equal(feeUsed, 165720);
        });
      });

      it('approximate with feeRate set by feeTxConfirmTarget fallback (estimatefee monkeypatch)', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
        return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients, feeTxConfirmTarget: 4, noSplitChange: true })
        .then(function(result) {
          var feeUsed = result.fee;
          assert.equal(feeUsed, 7532700); // tx size will be 75kb * 0.001 (max feerate as defined in transactionBuilder)
        });
      });

      it('validate (disable address verification)', function() {
        var manualFee = 0.04 * 1e8;
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6194e8;
        var walletmock = Object.create(wallet1);
        walletmock.createAddress = function(params) {
          assert.equal(params.validate, false);
          return wallet1.createAddress.apply(wallet1, arguments);
        };
        return TransactionBuilder.createTransaction({wallet: walletmock, recipients: recipients, fee: manualFee, validate: false})
        .then(function(result) {
          assert.equal(result.fee, manualFee);
        });
      });

      it('custom change address', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6194e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeRate: 0.0002 * 1e8, forceChangeAtEnd: true, changeAddress: TestBitGo.TEST_WALLET1_ADDRESS})
        .then(function(result) {
          assert.equal(result.changeAddresses[0].address, TestBitGo.TEST_WALLET1_ADDRESS);
        });
      });

      it('no change splitting', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6194e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeRate: 0.0002 * 1e8, forceChangeAtEnd: true, noSplitChange: true })
        .then(function(result) {
          result.changeAddresses.length.should.equal(1);
          result.should.have.property('txInfo');
          result.txInfo.nP2SHInputs.should.eql(255);
          result.txInfo.nP2PKHInputs.should.eql(0);
          result.txInfo.nOutputs.should.eql(3);
        });
      });

      it('no change splitting 2', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6194e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeRate: 0.0002 * 1e8, forceChangeAtEnd: true, splitChangeSize: 0 })
        .then(function(result) {
          result.changeAddresses.length.should.equal(1);
        });
      });

      it('change splitting on by default', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6194e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeRate: 0.0002 * 1e8, forceChangeAtEnd: true })
        .then(function(result) {
          result.changeAddresses.length.should.equal(3);
          result.should.have.property('txInfo');
          result.txInfo.nP2SHInputs.should.eql(255);
          result.txInfo.nP2PKHInputs.should.eql(0);
          result.txInfo.nOutputs.should.eql(5);
        });
      });

      it('insufficient inputs in single key address', function() {
        var recipients = [];
        recipients.push({ address: 'n3Eii3DYh5z3SMzWiq7ZVS43bQLvuArsd4', script: '76a914ee40c53bd6f0dcc34f024b6dd13803db2bc8beba88ac', amount: 0.01 * 1e8 });
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.1 * 1e8;

        return TransactionBuilder.createTransaction({
          wallet: wallet1,
          recipients: recipients,
          feeSingleKeySourceAddress: "mnGmNgALrkHRX6nPqmC4x1tmGtJn9sFTdn"
        })
        .then(function(result) {
          throw new Error('succeeded');
        })
        .catch(function(err) {
          err.message.should.eql('No unspents available in single key fee source');
        })
        .done();
      });

      it('single key address and WIF do not match', function() {
        var recipients = [];
        recipients.push({ address: 'n3Eii3DYh5z3SMzWiq7ZVS43bQLvuArsd4', script: '76a914ee40c53bd6f0dcc34f024b6dd13803db2bc8beba88ac', amount: 0.01 * 1e8 });
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.01 * 1e8;

        return Q()
        .then(function() {
          return TransactionBuilder.createTransaction({
            wallet: wallet1,
            recipients: recipients,
            feeSingleKeySourceAddress: "mibJ4uJc9f1fbMeaUXNuWqsB1JgNMcTZK7",
            feeSingleKeyWIF: "L2zRizgTckt4FbBae1AUcxMC686S37iACpiAj4aMEiUtxKFhW87q"
          });
        })
        .then(function(result) {
          throw new Error('succeeded');
        })
        .catch(function(err) {
          err.message.should.eql('feeSingleKeySourceAddress did not correspond to address of feeSingleKeyWIF');
        })
        .done();
      });

      it('ok with single fee wallet key', function() {
        var recipients = [];
        recipients.push({ address: 'n3Eii3DYh5z3SMzWiq7ZVS43bQLvuArsd4', script: '76a914ee40c53bd6f0dcc34f024b6dd13803db2bc8beba88ac', amount: 0.01 * 1e8 });
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.01 * 1e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeSingleKeyWIF: "cRVQ6cbUyGHVvByPKF9GnEhaB4HUBFgLQ2jVX1kbQARHaTaD7WJ2", splitChangeSize: 0})
        .then(function(result) {
          result.should.have.property('unspents');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.walletId.should.equal(wallet1.id());
          result.unspents[result.unspents.length - 1].redeemScript.should.eql(false);
          result.changeAddresses.length.should.eql(2); // we expect 2 changeaddresses - 1 for the usual wallet, and 1 for the fee address
          result.should.have.property('txInfo');
          result.txInfo.nP2SHInputs.should.eql(1);
          result.txInfo.nP2PKHInputs.should.eql(1);
          result.txInfo.nOutputs.should.eql(3);
        });
      });

      it('ok with single fee wallet address', function() {
        var recipients = [];
        recipients.push({ address: 'n3Eii3DYh5z3SMzWiq7ZVS43bQLvuArsd4', script: '76a914ee40c53bd6f0dcc34f024b6dd13803db2bc8beba88ac', amount: 0.01 * 1e8 });
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.01 * 1e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeSingleKeySourceAddress: "mibJ4uJc9f1fbMeaUXNuWqsB1JgNMcTZK7", splitChangeSize: 0})
        .then(function(result) {
          result.should.have.property('unspents');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.walletId.should.equal(wallet1.id());
          result.unspents[result.unspents.length - 1].redeemScript.should.eql(false);
          result.changeAddresses.length.should.eql(2); // we expect 2 changeaddresses - 1 for the usual wallet, and 1 for the fee address

          // parse tx to make sure the single key address was used to pay the fee
          var transaction = bitcoin.Transaction.fromHex(result.transactionHex);
          var singleKeyInput = transaction.ins[transaction.ins.length - 1];
          var inputTxHash = bitcoin.bufferutils.reverse(singleKeyInput.hash).toString('hex');

          // get the input tx to find the amount taken from the single key fee address
          return bitgo.get(bitgo.url('/tx/' + inputTxHash))
          .then(function(response) {
            var inputTx = response.body;
            var output = inputTx.outputs[singleKeyInput.index];

            var feeAddressInputValue = output.value;
            var feeAddressChangeAmount = _.find(result.changeAddresses, { address: 'mibJ4uJc9f1fbMeaUXNuWqsB1JgNMcTZK7' }).amount;

            // calculate the implied fee by using the input amount minus the output and ensure this amount was the final fee for the tx
            var impliedFeeFromTx = feeAddressInputValue - feeAddressChangeAmount;
            impliedFeeFromTx.should.eql(result.fee);
          })
        });
      });

      it('ok with single fee wallet address and key', function() {
        var recipients = [];
        recipients.push({ address: 'n3Eii3DYh5z3SMzWiq7ZVS43bQLvuArsd4', script: '76a914ee40c53bd6f0dcc34f024b6dd13803db2bc8beba88ac', amount: 0.01 * 1e8 });
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.01 * 1e8;
        return TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeSingleKeySourceAddress: "mibJ4uJc9f1fbMeaUXNuWqsB1JgNMcTZK7", feeSingleKeyWIF: "cRVQ6cbUyGHVvByPKF9GnEhaB4HUBFgLQ2jVX1kbQARHaTaD7WJ2"})
        .then(function(result) {
          result.should.have.property('unspents');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.walletId.should.equal(wallet1.id());
          result.unspents[result.unspents.length - 1].redeemScript.should.eql(false);
          result.changeAddresses.length.should.be.greaterThan(2); // we expect more than 2 changeaddresses - 2 for the usual wallet (autodetected split change size), and 1 for the fee address
        });
      });
    });

    describe('sign', function() {
      var unsignedTransaction;
      var unsignedTransactionUsingSingleKeyFeeAddress;
      var keychain;
      before(function(done) {

        bitgo.unlock({ otp: bitgo.testUserOTP() }, function(err) {
          assert.equal(err, null);
          // Go fetch the private key for our keychain
          var options = {
            xpub: wallet1.keychains[0].xpub,
          };
          bitgo.keychains().get(options, function(err, result) {
            assert.equal(err, null);
            keychain = result;

            var recipients = {};
            recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.001 * 1e8;

            // Now build a transaction
            TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients})
            .then(function(result) {
              unsignedTransaction = result;
              // Build a transaction with single fee wallet address
              TransactionBuilder.createTransaction({wallet: wallet1, recipients: recipients, feeSingleKeySourceAddress: TestBitGo.TEST_FEE_SINGLE_KEY_ADDRESS })
              .then(function(result) {
                unsignedTransactionUsingSingleKeyFeeAddress = result;
                done();
              })
              .done();
            })
            .done();
          });
        });
      });

      it('arguments', function() {
        var bogusKey = 'xprv9s21ZrQH143K2EPMtV8YHh3UzYdidYbQyNgxAcEVg1374nZs7UWRvoPRT2tdYpN6dENTZbBNf4Af3ZJQbKDydh1BmZ6azhFeYKJ3knPPjND';
        assert.throws(function() { TransactionBuilder.signTransaction(); });
        assert.throws(function() { TransactionBuilder.signTransaction({transactionHex: 'somestring'}); });
        assert.throws(function() { TransactionBuilder.signTransaction({transactionHex: []}); });
        assert.throws(function() { TransactionBuilder.signTransaction({transactionHex: 'somestring', unspents: [], keychain: boguskey}); });
        assert.throws(function() { TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: {}}); });
        assert.throws(function() { TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: 'asdfasdds', keychain: boguskey}); });
        assert.throws(function() { TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: {}, keychain: boguskey}); });
      });

      it('invalid key', function(done) {
        var bogusKey = 'xprv9s21ZrQH143K2EPMtV8YHh3UzYdidYbQyNgxAcEVg1374nZs7UWRvoPRT2tdYpN6dENTZbBNf4Af3ZJQbKDydh1BmZ6azhFeYKJ3knPPjND';
        assert.throws(function() {
          TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: unsignedTransaction.unspents, keychain: bogusKey}); }
        );
        done();
      });

      it('valid key', function(done) {
        // First we need to decrypt the xprv.
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        // Now we can go ahead and sign.
        TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: unsignedTransaction.unspents, keychain: keychain})
        .then(function(result) {
          result.transactionHex.should.not.eql("");
          result.transactionHex.should.not.eql(unsignedTransaction.transactionHex);
          result.transactionHex.length.should.be.above(unsignedTransaction.transactionHex.length);
          done();
        })
        .done();
      });

      it('valid key but missing single-sig key', function() {
        // First we need to decrypt the xprv.
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        // Now we can go ahead and sign.
        return Q()
        .then(function() {
          return TransactionBuilder.signTransaction({
            transactionHex: unsignedTransactionUsingSingleKeyFeeAddress.transactionHex,
            unspents: unsignedTransactionUsingSingleKeyFeeAddress.unspents,
            keychain: keychain,
            feeSingleKeySourceAddress: TestBitGo.TEST_FEE_SINGLE_KEY_ADDRESS
          });
        })
        .then(function(res) {
          throw new Error('succeeded');
        })
        .catch(function(e) {
          e.message.should.eql('single key address used in input but feeSingleKeyWIF not provided');
        });
      });

      it('invalid single-sig key WIF', function() {
        // First we need to decrypt the xprv.
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        // Now we can go ahead and sign.
        return Q()
        .then(function() {
          return TransactionBuilder.signTransaction({
            transactionHex: unsignedTransactionUsingSingleKeyFeeAddress.transactionHex,
            unspents: unsignedTransactionUsingSingleKeyFeeAddress.unspents,
            keychain: keychain,
            feeSingleKeyWIF: 'L18QdhbdYCbEkkW7vqL9QvCWYpz4WoaeKzb2QbJ5u3mHKiSoqk98'
          });
        })
        .then(function(res) {
          throw new Error('succeeded');
        })
        .catch(function(e) {
          e.message.should.eql('Invalid checksum');
        });
      });

      it('valid key and valid single-sig key WIF', function() {
        // First we need to decrypt the xprv.
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        // Now we can go ahead and sign.
        return TransactionBuilder.signTransaction({
          transactionHex: unsignedTransactionUsingSingleKeyFeeAddress.transactionHex,
          unspents: unsignedTransactionUsingSingleKeyFeeAddress.unspents,
          keychain: keychain,
          feeSingleKeyWIF: TestBitGo.TEST_FEE_SINGLE_KEY_WIF
        })
        .then(function(result) {
          result.transactionHex.should.not.eql("");
          result.transactionHex.should.not.eql(unsignedTransaction.transactionHex);
          result.transactionHex.length.should.be.above(unsignedTransaction.transactionHex.length);
        });
      });

      it('validate (disable signature verification)', function() {
        // First we need to decrypt the xprv.
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        // Now we can go ahead and sign.
        var realVerifyInputSignatures = TransactionBuilder.verifyInputSignatures;
        TransactionBuilder.verifyInputSignatures = function() {
          throw new Error('should not be called');
        };
        return TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: unsignedTransaction.unspents, keychain: keychain, validate: false})
        .then(function(result) {
          // restore object's true method for the other tests
          TransactionBuilder.verifyInputSignatures = realVerifyInputSignatures;
          result.transactionHex.should.not.eql("");
          result.transactionHex.should.not.eql(unsignedTransaction.transactionHex);
          result.transactionHex.length.should.be.above(unsignedTransaction.transactionHex.length);
        });
      });

      it('validate (enable signature verification)', function() {
        // First we need to decrypt the xprv.
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        // Now we can go ahead and sign.
        var realVerifyInputSignatures = TransactionBuilder.verifyInputSignatures;
        var verifyWasCalled = false;
        TransactionBuilder.verifyInputSignatures = function() {
          verifyWasCalled = true;
          return -1;
        };
        return TransactionBuilder.signTransaction({transactionHex: unsignedTransaction.transactionHex, unspents: unsignedTransaction.unspents, keychain: keychain, validate: true})
        .then(function(result) {
          // restore object's true method for the other tests
          TransactionBuilder.verifyInputSignatures = realVerifyInputSignatures;
          assert.equal(verifyWasCalled, true);
        });
      });

    });
  });

  describe('Get wallet user encrypted key', function() {
    it('arguments', function(done) {
      assert.throws(function() { wallet1.getEncryptedUserKeychain(undefined, 'invalid'); });
      assert.throws(function() { wallet1.getEncryptedUserKeychain({}, 'invalid'); });
      assert.throws(function() { wallet1.transactions('invalid', function() {}); });
      done();
    });

    it('get key', function(done) {
      var options = { };
      wallet1.getEncryptedUserKeychain(options, function(err, result) {
        assert.equal(err, null);
        result.should.have.property('xpub');
        assert.equal(result.xpub, TestBitGo.TEST_WALLET1_XPUB);
        result.should.have.property('encryptedXprv');
        done();
      });
    });
  });

  describe('Send coins', function() {
    it('arguments', function () {
      assert.throws(function () {
        wallet1.sendCoins();
      });
      assert.throws(function () {
        wallet1.sendCoins({ address: 123 });
      });
      assert.throws(function () {
        wallet1.sendCoins({ address: 'string' });
      });

      return wallet1.sendCoins({ address: 'string', amount: 123 })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad code");
      })
      .catch(function(err) {
        err.message.should.include('one of xprv or walletPassphrase');
        return wallet1.sendCoins({ address: 'string', amount: 123, walletPassphrase: ' '});
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad address");
      })
      .catch(function(err) {
        err.message.should.include('invalid bitcoin address');
        return wallet1.sendCoins({ address: 'string', amount: 123, walletPassphrase: 'advanced1' }, {});
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad code");
      })
      .catch(function(err) {
        err.message.should.include('illegal callback argument');
        return wallet1.sendCoins({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 1, walletPassphrase: 'badcode' });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad code");
      })
      .catch(function(err) {
        err.message.should.include('Unable to decrypt user keychain');
        return wallet1.sendCoins({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: -1, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad amount");
      })
      .catch(function(err) {
        err.message.should.include('invalid amount');
        return wallet1.sendCoins({ address: "bad address", amount: 1, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad address");
      })
      .catch(function(err) {
        err.message.should.include('invalid bitcoin address');
        return wallet1.sendCoins({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 10000, xprv: "abcdef" });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad xprv");
      })
      .catch(function(err) {
        err.message.should.include("Unable to parse");
        return wallet1.sendCoins({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 10000, xprv: "xprv9wHokC2KXdTSpEepFcu53hMDUHYfAtTaLEJEMyxBPAMf78hJg17WhL5FyeDUQH5KWmGjGgEb2j74gsZqgupWpPbZgP6uFmP8MYEy5BNbyET" })
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch xprv not belonging on wallet");
      })
      .catch(function(err) {
        err.message.should.include("not a keychain on this wallet");
        return wallet1.sendCoins({ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 10000, xprv: "xpub661MyMwAqRbcGU7FnXMKSHMwbWxARxYJUpKD1CoMJP6vonLT9bZZaWYq7A7tKPXmDFFXTKigT7VHMnbtEnjCmxQ1E93ZJe6HDKwxWD28M6f" })
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch xpub provided instead of xprv");
      })
      .catch(function(err) {
        err.message.should.include("not a private key");
      });
    });

    describe('Bad input', function () {
      it('send coins - insufficient funds', function () {
        return wallet1.sendCoins(
          { address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 22 * 1e8 * 1e8, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE }
        )
        .then(function(res) {
          assert(false); // should not reach
        })
        .catch(function(err) {
          err.message.should.eql('Insufficient funds');
          err.result.should.have.property('txInfo');
          err.result.txInfo.should.have.property('nP2SHInputs');
          err.result.txInfo.should.have.property('nP2PKHInputs');
          err.result.txInfo.should.have.property('nOutputs');
          err.result.txInfo.nP2PKHInputs.should.eql(0);
        });
      });

      it('send coins - instant unsupported on non-krs wallet', function() {
        return wallet1.sendCoins({
          address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 0.001 * 1e8 * 1e8, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE, instant: true
        })
        .then(function(res) {
          assert(false); // should not reach
        })
        .catch(function(err) {
          err.message.should.eql('wallet does not support instant transactions');
        });
      });
    });

    describe('Real transactions', function() {
      it('send coins fails - not unlocked', function () {
        return bitgo.lock({})
        .then(function() {
          return wallet1.sendCoins(
            { address: TestBitGo.TEST_WALLET3_ADDRESS, amount: 0.006 * 1e8, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE }
          );
        })
        .then(function(result) {
          assert(false); // should not reach
        })
        .catch(function(err) {
          err.needsOTP.should.equal(true);
        });
      });

      it('send coins - wallet1 to wallet3', function () {
        return bitgo.unlock({ otp: '0000000' })
        .then(function() {
          return wallet1.sendCoins(
            { address: TestBitGo.TEST_WALLET3_ADDRESS, amount: 0.006 * 1e8, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE }
          );
        })
        .then(function(result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.should.have.property('instant');
          result.instant.should.eql(false);
          result.feeRate.should.be.lessThan(0.01*1e8);
        });
      });

      it('send instant transaction with no fee required - wallet3 to wallet1', function() {
        return wallet3.sendCoins({
          address: TestBitGo.TEST_WALLET1_ADDRESS,
          amount: 0.001 * 1e8,
          walletPassphrase: TestBitGo.TEST_WALLET3_PASSCODE,
          instant: true
        })
        .then(function(result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('instant');
          result.should.have.property('instantId');
          result.should.not.have.property('bitgoFee');
          result.instant.should.eql(true);
        });
      });

      it('send coins - wallet1 to wallet3 using xprv and single key fee input', function () {
        var seqId = Math.floor(Math.random()*1e16).toString(16);
        var txHash;
        return bitgo.unlock({ otp: '0000000' })
        .then(function() {
          return wallet1.sendCoins({
            address: TestBitGo.TEST_WALLET3_ADDRESS, amount: 14000000, // 0.14 coins, test js floating point bugs
            xprv: "xprv9s21ZrQH143K3z2ngVpK59RD3V7g2VpT7bPcCpPjk3Zwvz1Jc4FK2iEMFtKeWMfgDRpqQosVgqS7NNXhA3iVYjn8sd9mxUpx4wFFsMxxWEi",
            sequenceId: seqId,
            feeSingleKeyWIF: TestBitGo.TEST_FEE_SINGLE_KEY_WIF
          });
        })
        .then(function(result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.feeRate.should.be.lessThan(0.01*1e8);
          txHash = result.hash;
          return wallet1.getWalletTransactionBySequenceId({ sequenceId: seqId });
        })
        .then(function(result) {
          result.transaction.transactionId.should.eql(txHash);
          result.transaction.sequenceId.should.eql(seqId);
        });
      });

      it('send coins - wallet3 to wallet1 with xprv and instant', function() {
        return wallet3.sendCoins({
          address: TestBitGo.TEST_WALLET1_ADDRESS, amount: 14000000, // 0.14 coins, test js floating point bugs
          xprv: "xprv9s21ZrQH143K3aLCRoCteo8TkJWojD5d8wQwJmcvUPx6TaDeLnEWq2Mw6ffDyThZNe4YgaNsdEAL9JN8ip8BdqisQsEpy9yR6HxVfvkgEEZ"
        })
        .then(function(result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
        });
      });

      it('list unspents and expect some instant and some non-instant', function() {
        return wallet3.unspents({})
        .then(function(unspents) {
          _.some(unspents, function(unspent) { return unspent.instant === true; }).should.eql(true);
          _.some(unspents, function(unspent) { return unspent.instant === false; }).should.eql(true);
        });
      });

      it('get instant balance 2 ways and make sure they are the same', function() {
        var instantBalanceFromUnspentsNative;
        return wallet3.get()
        .then(function() {
          return wallet3.unspents({ instant: true, target: 10000 * 1e8 });
        })
        .then(function(unspents) {
          instantBalanceFromUnspentsNative = _.sumBy(unspents, 'value');
          return wallet3.instantBalance();
        })
        .then(function(balance) {
          balance.should.eql(instantBalanceFromUnspentsNative);
        });
      });
    });
  });

  describe('Send many', function() {
    it('arguments', function () {
      assert.throws(function () {
        wallet1.sendMany();
      });
      assert.throws(function () {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.001 * 1e8;
        wallet1.sendMany([ { recipients: recipients, walletPassphrase: "badpasscode" } ], function () {});
      });

      return wallet1.sendMany({ recipients: { 'string': 123 }, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad recipient");
      })
      .catch(function(err) {
        err.message.should.include("invalid bitcoin address");
        return wallet1.sendMany({ recipients: [ 'string' ], walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad recipient");
      })
      .catch(function(err) {
        err.message.should.include("invalid amount");
        return wallet1.sendMany({ recipients: [{ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 12300 }], walletPassphrase: "abc" });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad wallet passphrase");
      })
      .catch(function(err) {
        err.message.should.include("Unable to decrypt user keychain");
        return wallet1.sendMany({ recipients: { 'string': 123 }, walletPassphrase: 'advanced1' }, {});
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad callback");
      })
      .catch(function(err) {
        err.message.should.include("illegal callback argument");
        return wallet1.sendMany({ recipients: [{ address: 'bad address', amount: 0.001 * 1e8 }], walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch single bad address");
      })
      .catch(function(err) {
        err.message.should.include("invalid bitcoin address");
        return wallet1.sendMany({ recipients: [{ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 0.001 * 1e8 }], walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE, xprv: "xprv9wHokC2KXdTSpEepFcu53hMDUHYfAtTaLEJEMyxBPAMf78hJg17WhL5FyeDUQH5KWmGjGgEb2j74gsZqgupWpPbZgP6uFmP8MYEy5BNbyET" });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch double usage of xprv/walletpassphrase");
      })
      .catch(function(err) {
        err.message.should.include('one of xprv or walletPassphrase');
        return wallet1.sendMany({ recipients: [{ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 0.001 * 1e8 }, { address: 'bad address', amount: 0.001 * 1e8 }], walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad address");
      })
      .catch(function(err) {
        err.message.should.include("invalid bitcoin address");
        return wallet1.sendMany({ recipients: [{ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: -100 }], walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE });
      })
      .then(function(result) {
        throw new Error("Unexpected result - expected to catch bad amount");
      })
      .catch(function(err) {
        err.message.should.include("invalid amount");
      });
    });

    describe('Bad input', function () {
      it('send many - insufficient funds', function (done) {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.001 * 1e8;
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 22 * 1e8 * 1e8;
        wallet1.sendMany(
        { recipients: recipients, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE },
        function (err, result) {
          assert.notEqual(err, null);
          done();
        }
        );
      });
    });

    describe('Real transactions', function() {
      it('send to legacy safe wallet from wallet1', function (done) {
        var recipients = {};
        recipients["2MvfC3e6njdTXqWDfGvNUqDs5kwimfaTGjK"] = 0.001 * 1e8;
        wallet1.sendMany(
        { recipients: recipients, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE },
        function (err, result) {
          assert.equal(err, null);
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          result.feeRate.should.be.lessThan(0.01*1e8);
          done();
        });
      });

      it('send from legacy safe wallet back to wallet1', function () {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.0009 * 1e8;
        return safewallet.createTransaction({recipients: recipients})
        .then(function(tx) {
          var enc = '{"iv":"lFkIIulsbL+Ub2jGUiXdrw==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"pdx6d0iD+Io=","ct":"kVIZBeHxoxt19ki0hs5WBjmuLdHPfBQ30a0iGb5H+pR6+kH5lr3zxPL0xeO5EtwPRR0Mw0JVuLqapQE="}';
          var decrypted = bitgo.decrypt({ password: TestBitGo.TEST_PASSWORD, input: enc });
          tx.signingKey = decrypted;
          return safewallet.signTransaction(tx);
        })
        .then(function(result) {
          result.should.have.property('tx');
        });
      });

      it('send many - wallet1 to wallet3 (single output)', function (done) {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET3_ADDRESS] = 0.001 * 1e8;
        wallet1.sendMany(
        { recipients: recipients, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE },
        function (err, result) {
          assert.equal(err, null);
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          done();
        }
        );
      });

      it('send many - wallet3 to wallet1 (single output, using xprv instead of passphrase)', function () {
        var recipients = [];
        recipients.push({ address: TestBitGo.TEST_WALLET1_ADDRESS, amount: 0.001 * 1e8});
        return wallet3.sendMany({
          recipients: recipients,
          xprv: "xprv9s21ZrQH143K3aLCRoCteo8TkJWojD5d8wQwJmcvUPx6TaDeLnEWq2Mw6ffDyThZNe4YgaNsdEAL9JN8ip8BdqisQsEpy9yR6HxVfvkgEEZ"
        })
        .then(function (result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
        });
      });

      it('send many - wallet3 to wallet1 (single output, using keychain)', function () {
        var recipients = [];
        recipients.push({ address: TestBitGo.TEST_WALLET1_ADDRESS, amount: 0.001 * 1e8});
        return wallet3.getEncryptedUserKeychain()
        .then(function(keychain) {
          keychain.xprv = 'xprv9s21ZrQH143K3aLCRoCteo8TkJWojD5d8wQwJmcvUPx6TaDeLnEWq2Mw6ffDyThZNe4YgaNsdEAL9JN8ip8BdqisQsEpy9yR6HxVfvkgEEZ';
          return wallet3.sendMany({ recipients: recipients, keychain: keychain });
        })
        .then(function (result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.should.have.property('feeRate');
        });
      });

      it('send many - wallet1 to wallet3 with dynamic fee', function (done) {
        var recipients = [];
        recipients.push({ address: TestBitGo.TEST_WALLET3_ADDRESS, amount: 0.001 * 1e8});
        recipients.push({ address: TestBitGo.TEST_WALLET3_ADDRESS2, amount: 0.001 * 1e8});
        recipients.push({ address: TestBitGo.TEST_WALLET3_ADDRESS3, amount: 0.006 * 1e8});
        wallet1.sendMany(
        { recipients: recipients, walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE, feeTxConfirmTarget: 2 },
        function (err, result) {
          assert.equal(err, null);
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          done();
        }
        );
      });

      it('send many - wallet1 to wallet3 with travel info', function () {
        var recipients = [];
        recipients.push({
          address: TestBitGo.TEST_WALLET3_ADDRESS,
          amount: 0.001 * 1e8,
          travelInfo: {
            fromUserName: 'Alice'
          }
        });
        recipients.push({
          address: TestBitGo.TEST_WALLET3_ADDRESS2,
          amount: 0.002 * 1e8,
          travelInfo: {
            toUserName: 'Bob'
          }
        });
        recipients.push({ address: TestBitGo.TEST_WALLET3_ADDRESS3, amount: 0.006 * 1e8});
        return wallet1.sendMany({
          recipients: recipients,
          walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE,
          feeTxConfirmTarget: 2 }
        )
        .then(function(res) {
          res.should.have.property('tx');
          res.should.have.property('hash');
          res.should.have.property('fee');
          res.should.have.property('travelResult');
          res.travelResult.matched.should.equal(2);
          res.travelResult.results.should.have.length(2);
          var result = res.travelResult.results[0].result;
          result.should.have.property('id');
          result.fromEnterprise.should.equal('SDKTest');
          result.fromEnterpriseId.should.equal(TestBitGo.TEST_ENTERPRISE);
          result.toEnterpriseId.should.equal(TestBitGo.TEST_ENTERPRISE_2);
          result.transactionId.should.equal(res.hash);
          result.should.have.property('outputIndex');
          result.fromWallet.should.equal(TestBitGo.TEST_WALLET1_ADDRESS);
          result.toAddress.should.equal(TestBitGo.TEST_WALLET3_ADDRESS);
          result.amount.should.equal(100000);
          result.toPubKey.should.equal('02fbb4b2f489535af4660202836ec041f2751700bfa1e65a72dee039b7ae3a3ac3');
          result.should.have.property('encryptedTravelInfo');
          result = res.travelResult.results[1].result;
          result.amount.should.equal(200000);
          result.should.have.property('encryptedTravelInfo');
        });
      });

      it('send many - wallet3 to wallet1 with specified fee', function () {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.001 * 1e8;
        recipients[TestBitGo.TEST_WALLET1_ADDRESS2] = 0.002 * 1e8;
        return wallet3.sendMany({
          recipients: recipients,
          walletPassphrase: TestBitGo.TEST_WALLET3_PASSCODE,
          fee: 0.00042 * 1e8
        })
        .then(function(result) {
          result.should.have.property('tx');
          result.should.have.property('hash');
          result.should.have.property('fee');
          result.fee.should.equal(0.00042 * 1e8);
          return wallet3.get({});
        })
        .then(function(resultWallet) {
          resultWallet.unconfirmedReceives().should.not.eql(0);
          resultWallet.unconfirmedSends().should.not.eql(0);
        });
      });
    });
  });

  describe('Create and Send Transactions (advanced)', function() {
    var keychain;
    var tx;

    before(function(done) {

      // Set up keychain
      var options = {
        xpub: wallet1.keychains[0].xpub
      };
      bitgo.keychains().get(options, function(err, result) {
        assert.equal(err, null);
        keychain = result;
        done();
      });
    });

    it('arguments', function(done) {
      assert.throws(function() { wallet1.createTransaction(); });
      assert.throws(function() { wallet1.createTransaction({ recipients: [ 123 ] }); });
      assert.throws(function() { wallet1.createTransaction({ recipients: { 123: true } }); });
      assert.throws(function() { wallet1.createTransaction({ recipients: { 'string': 123 } }); });
      assert.throws(function() { wallet1.createTransaction({ recipients: { 'string': 123 }, fee: 0}); });
      assert.throws(function() { wallet1.createTransaction({ recipients: { 'string': 123 }, fee: 0, keychain: {} }); });
      assert.throws(function() { wallet1.createTransaction({ address: 'string', amount: 123, fee: 0, keychain: {} }); });

      assert.throws(function() { wallet1.createTransaction({ recipients: { 'invalidaddress': 0.001 * 1e8 }, fee: 0.0001 * 1e8, keychain: keychain }); })
      assert.throws(function() { wallet1.signTransaction(); });
      assert.throws(function() { wallet1.signTransaction({}); });
      assert.throws(function() { wallet1.signTransaction({ keychain:'111' }); });
      assert.throws(function() { wallet1.signTransaction({ transactionHex:'111' }); });
      assert.throws(function() { wallet1.signTransaction({ unspents: [] }); });
      assert.throws(function() { wallet1.signTransaction({ transactionHex:'111', unspents: [], keychain: { xprv: 'abc' } }); });

      assert.throws(function() { wallet1.sendTransaction(); });
      assert.throws(function() { wallet1.sendTransaction({}); });

      assert.throws(function () { wallet1.createTransaction({ recipients: {}, fee: 0.0001 * 1e8, keychain: keychain }, function() {} );});
      done();
    });

    describe('full transaction', function() {
      it('decrypt key', function(done) {
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET1_PASSCODE, input: keychain.encryptedXprv });
        done();
      });

      it('create and sign transaction with global no validation', function() {
        var recipients = [];
        recipients.push({
          address: TestBitGo.TEST_WALLET2_ADDRESS,
          amount: 0.001 * 1e8
        });
        var calledVerify = false;
        var setValidate = false;
        var realVerifyInputSignatures = TransactionBuilder.verifyInputSignatures;
        TransactionBuilder.verifyInputSignatures = function() {
          calledVerify = true;
          return -1;
        };
        var wallet = Object.create(wallet1);
        wallet.createAddress = function(params) {
          params.validate.should.equal(false);
          setValidate = true;
          return wallet1.createAddress.apply(wallet, arguments);
        };
        wallet.bitgo.setValidate(false);
        return wallet.createTransaction({ recipients: recipients })
        .then(function(result) {
          result.should.have.property('fee');
          return wallet.signTransaction({ transactionHex: result.transactionHex, unspents: result.unspents, keychain: keychain });
        })
        .then(function(result) {
          TransactionBuilder.verifyInputSignatures = realVerifyInputSignatures;
          calledVerify.should.equal(false);
          setValidate.should.equal(true);
          result.should.have.property('tx');
          tx = result.tx;
        });
      });

      it('create tx with bad travelInfo', function() {
        var recipients = [];
        recipients.push({
          address: TestBitGo.TEST_WALLET2_ADDRESS,
          amount: 0.001 * 1e8,
          travelInfo: {
            fromUserName: 42
          }
        });
        var wallet = Object.create(wallet1);
        return wallet.createTransaction({ recipients: recipients })
        .then(function(result) {
          // should not reach
          assert(false);
        })
        .catch(function(err) {
          err.message.should.include('incorrect type for field fromUserName in travel info');
        });
      });

      it('create tx with travelInfo', function() {
        var recipients = [];
        recipients.push({
          address: TestBitGo.TEST_WALLET2_ADDRESS,
          amount: 0.001 * 1e8,
          travelInfo: {
            fromUserName: 'Alice',
            toUserName: 'Bob'
          }
        });
        var wallet = Object.create(wallet1);
        return wallet.createTransaction({ recipients: recipients })
        .then(function(res) {
          res.should.have.property('travelInfos');
          res.travelInfos.should.have.length(1);
          var travelInfo = res.travelInfos[0];
          travelInfo.should.have.property('outputIndex');
          travelInfo.fromUserName.should.equal('Alice');
          travelInfo.toUserName.should.equal('Bob');
        });
      });

      it('create and sign transaction with fee', function(done) {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.001 * 1e8;
        wallet1.createTransaction({ recipients: recipients, fee: 0.0001 * 1e8 })
        .then(function(result) {
          result.should.have.property('fee');
          assert.equal(result.fee < 0.0005 * 1e8, true);
          return wallet1.signTransaction({ transactionHex: result.transactionHex, unspents: result.unspents, keychain: keychain });
        })
        .then(function(result) {
          result.should.have.property('tx');
          tx = result.tx;
        })
        .done(done);
      });

      it('create and sign transaction with default fee', function(done) {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 0.001 * 1e8;
        wallet1.createTransaction({ recipients: recipients })
        .then(function(result) {
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          should.exist(result.fee);
          result.fee.should.be.lessThan(0.01*1e8);
          result.feeRate.should.be.lessThan(0.01*1e8);
          return wallet1.signTransaction({ transactionHex: result.transactionHex, unspents: result.unspents, keychain: keychain });
        })
        .then(function(result) {
          result.should.have.property('tx');
          tx = result.tx;
        })
        .done(done);
      });

      it('create instant transaction', function() {
        var recipients = {};
        var bitgoFee;
        recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 1.1e8;
        return wallet3.createTransaction({ recipients: recipients, instant: true })
        .then(function(result) {
          result.should.have.property('fee');
          result.should.have.property('feeRate');
          should.exist(result.fee);
          result.fee.should.be.lessThan(0.01e8);
          result.feeRate.should.be.lessThan(0.01e8);
          result.should.have.property('bitgoFee');
          bitgoFee = result.bitgoFee;
          bitgoFee.amount.should.equal(110000);
          bitgoFee.should.have.property('address');
          result.should.have.property('instantFee');
          result.instantFee.amount.should.equal(110000);
          // Re-create the same tx, passing bitgoFee info
          return wallet3.createTransaction({ recipients: recipients, instant: true, bitgoFee: result.bitgoFee});
        })
        .then(function(result) {
          result.should.have.property('bitgoFee');
          result.bitgoFee.address.should.equal(bitgoFee.address);
        });
      });

      it('send', function(done) {
        wallet1.sendTransaction({ tx: tx }, function(err, result) {
          assert.equal(err, null);
          result.should.have.property('tx');
          result.should.have.property('hash');
          done();
        });
      });
    });

    // Now send the money back
    describe('return transaction', function() {
      var keychain;
      var tx;

      it('keychain', function(done) {
        var options = {
          xpub: wallet2.keychains[0].xpub
        };
        bitgo.keychains().get(options, function(err, result) {
          assert.equal(err, null);
          keychain = result;
          done();
        });
      });

      it('decrypt key', function(done) {
        keychain.xprv = bitgo.decrypt({ password: TestBitGo.TEST_WALLET2_PASSCODE, input: keychain.encryptedXprv });
        done();
      });

      it('create transaction, check that minSize is sent', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.001 * 1e8;
        // monkey patch unspents to check that expected options are sent
        var backingUnspentMethod = wallet2.unspents.bind(wallet2);
        wallet2.unspents = function(expectedOptions) {
          expectedOptions.should.have.property('minSize');
          expectedOptions.minSize.should.eql(5460);
          return backingUnspentMethod(arguments);
        };
        return wallet2.createTransaction({ recipients: recipients, fee: 0.0001 * 1e8, minConfirms: 1 })
        .then(function(result) {
          result.should.have.property('fee');
          return wallet2.signTransaction({ transactionHex: result.transactionHex, unspents: result.unspents, keychain: keychain });
        })
        .then(function(result) {
          result.should.have.property('tx');
          tx = result.tx;
          wallet2.unspents = backingUnspentMethod;
        });
      });

      it('create transaction with custom minSize', function() {
        var recipients = {};
        recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.001 * 1e8;
        // monkey patch unspents to check that expected options are sent
        var backingUnspentMethod = wallet2.unspents.bind(wallet2);
        wallet2.unspents = function(expectedOptions) {
          expectedOptions.should.have.property('minSize');
          expectedOptions.minSize.should.eql(999);
          return backingUnspentMethod(arguments);
        };
        return wallet2.createTransaction({ recipients: recipients, fee: 0.0001 * 1e8, minConfirms: 1, minUnspentSize: 999 })
        .then(function() {
          wallet2.unspents = backingUnspentMethod;
        });
      });

      it('send', function(done) {
        wallet2.sendTransaction({ tx: tx }, function(err, result) {
          assert.equal(err, null);
          result.should.have.property('tx');
          result.should.have.property('hash');
          done();
        });
      });
    });
  });

  describe('Policy', function() {
    it('arguments', function (done) {
      assert.throws(function () {
        wallet1.setPolicyRule({});
      });
      assert.throws(function () {
        wallet1.setPolicyRule({ id: 'policy1' });
      });
      assert.throws(function () {
        wallet1.setPolicyRule({ id: 'policy1', type: 'dailyLimit' });
      });
      assert.throws(function () {
        wallet1.setPolicyRule({ id: 'policy1', type: 'dailyLimit', action: { type: 'getApproval' }});
      });
      assert.throws(function () {
        wallet1.setPolicyRule({ id: 'policy1', type: 'dailyLimit', condition: { amount: 1e8 } });
      });
      assert.throws(function () {
        wallet1.removePolicyRule({});
      });
      done();
    });

    var amount;
    it('set a policy rule', function() {
      amount = 888 * 1e8 + Math.round(Math.random() * 1e8);
      return wallet1.setPolicyRule({
        action: { type: "getApproval" },
        condition: { "amount": amount },
        id: "test1",
        type: "dailyLimit"
      })
      .then(function(wallet) {
        wallet.id.should.eql(wallet1.id());
        var rulesById = _.keyBy(wallet.admin.policy.rules, 'id');
        rulesById.should.have.property('test1');
        rulesById['test1'].action.type.should.eql('getApproval');
        rulesById['test1'].condition.amount.should.eql(amount);
        rulesById['test1'].id.should.eql('test1');
        rulesById['test1'].type.should.eql('dailyLimit');
      });
    });

    it('get policy and rules', function() {
      return wallet1.getPolicy({})
      .then(function(policy) {
        var rulesById = _.keyBy(policy.rules, 'id');
        rulesById.should.have.property('test1');
        rulesById['test1'].action.type.should.eql('getApproval');
        rulesById['test1'].condition.amount.should.eql(amount);
        rulesById['test1'].id.should.eql('test1');
        rulesById['test1'].type.should.eql('dailyLimit');
      });
    });

    it('get policy status', function() {
      return wallet1.getPolicyStatus({})
      .then(function(policyStatus) {
        var rulesById = _.keyBy(policyStatus.statusResults, 'ruleId');
        rulesById['test1'].ruleId.should.eql('test1');
        rulesById['test1'].status.should.have.property('remaining');
        rulesById['test1'].status.remaining.should.be.greaterThan(0);
      });
    });

    it('delete the policy rule', function() {
      return wallet1.removePolicyRule({ id: 'test1' })
      .then(function(wallet) {
        wallet.id.should.eql(wallet1.id());
        var rulesById = _.keyBy(wallet.admin.policy.rules, 'id');
        rulesById.should.not.have.property('test1');
      });
    });
  });

  describe('Freeze Wallet', function() {
    it('arguments', function (done) {
      assert.throws(function () {
        wallet2.freeze({duration: 'asdfasdasd'});
      });
      assert.throws(function () {
        wallet2.freeze({duration: 5}, 'asdasdsa');
      });
      done();
    });

    it('perform freeze', function (done) {
      wallet2.freeze({duration: 6}, function (err, freezeResult) {
        freezeResult.should.have.property('time');
        freezeResult.should.have.property('expires');
        done();
      });
    });

    it('get wallet should show freeze', function (done) {
      wallet2.get({}, function (err, res) {
        var wallet = res.wallet;
        wallet.should.have.property('freeze');
        wallet.freeze.should.have.property('time');
        wallet.freeze.should.have.property('expires');
        done();
      });
    });

    it('attempt to send funds', function (done) {
      wallet2.sendCoins(
      {address: TestBitGo.TEST_WALLET3_ADDRESS, amount: 0.001 * 1e8, walletPassphrase: TestBitGo.TEST_WALLET2_PASSCODE},
      function (err, result) {
        err.should.not.equal(null);
        err.status.should.equal(403);
        err.message.should.include('wallet is frozen');
        done();
      }
      );
    });
  });
});

Выполнить команду


Для локальной разработки. Не используйте в интернете!