PHP WebShell
Текущая директория: /opt/BitGoJS/modules/bitgo/test/integration
Просмотр файла: wallet.ts
//
// Tests for Wallet
//
// Copyright 2014, BitGo, Inc. All Rights Reserved.
//
/* eslint-disable @typescript-eslint/no-empty-function */
import { strict as assert } from 'assert';
import * as should from 'should';
import { VirtualSizes } from '@bitgo/unspents';
import * as utxolib from '@bitgo/utxo-lib';
const Q = require('q');
const BitGoJS = require('../../src/index');
import { TestBitGo } from '../lib/test_bitgo';
const TransactionBuilder = require('../../src/transactionBuilder');
const crypto = require('crypto');
import * as _ from 'lodash';
const bitcoin = BitGoJS.bitcoin;
const unspentData = require('./fixtures/largeunspents.json');
const common = require('../../src/common');
const request = require('superagent');
const Wallet = require('../../src/wallet');
Q.longStackTrace = true;
describe('Wallet API', function () {
let bitgo;
let wallets;
let 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.
const options = {
id: TestBitGo.TEST_WALLET1_ADDRESS,
};
wallets.get(options, function (err, wallet) {
if (err) {
throw err;
}
wallet1 = wallet;
// Fetch the second wallet
const options = {
id: TestBitGo.TEST_WALLET2_ADDRESS,
};
wallets.get(options, function (err, wallet) {
if (err) {
throw err;
}
wallet2 = wallet;
// Fetch the third wallet
const options = {
id: TestBitGo.TEST_WALLET3_ADDRESS,
};
wallets.get(options, function (err, wallet) {
wallet3 = wallet;
// Fetch legacy safe wallet
const 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();
}
);
});
let 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);
}
);
});
});
let walletShareIdWithViewPermissions, walletShareIdWithSpendPermissions, cancelledWalletShareId;
describe('Share wallet', function () {
// clean up any outstanding shares before proceeding
before(function () {
return bitgo
.wallets()
.listShares({})
.then(function (result) {
const 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.containEql('Unable to decrypt user keychain');
done();
}
);
});
});
it('get sharing key for a user', function (done) {
const keychains = bitgo.keychains();
keychains.create();
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();
});
it('share a wallet and then resend 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());
const walletShareIdToResend = result.id;
return bitgo.wallets().resendShareInvite({ walletShareId: walletShareIdToResend }, function (err, result) {
result.should.have.property('resent', true);
done();
});
})
.done();
});
});
let 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 () {
let 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 () {
let 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) {
const 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');
const 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) {
const 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');
const 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 () {
const 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
const renameIndicator = crypto.randomBytes(3).toString('hex');
const originalWalletName = 'Even Better Test Wallet 1';
const 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 () {
// let sharedWallet;
before(function () {
const 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) {
const 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) {
const 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) {
const 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 () {
const 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 () {
const 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 () {
let regroupWallet;
before(function () {
const 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 () {
const 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 () {
const 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 () {
const 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);
const firstConsolidation = response[0];
firstConsolidation.should.have.property('hash');
firstConsolidation.should.have.property('tx');
firstConsolidation.status.should.equal('accepted');
});
});
xit('consolidate unspents', function () {
const maxInputCountPerConsolidation = 3;
let progressCallbackCount = 0;
const progressCallback = function (progressDetails) {
progressDetails.should.have.property('index');
progressDetails.should.have.property('inputCount');
progressDetails.index.should.equal(progressCallbackCount);
assert(progressDetails.inputCount <= maxInputCountPerConsolidation);
progressCallbackCount++;
};
const 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);
const 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.containEql('not an instant wallet');
});
});
it('wallet3 instant balance', function () {
const 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();
});
let txHash0;
it('list', function (done) {
const 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();
});
});
let limitedTxes;
const limitTestNumTx = 6;
let totalTxCount;
it('list with limit', function (done) {
const 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) {
const minHeight = 530000;
const 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 maxHeight', function (done) {
const maxHeight = 530000;
const options = { maxHeight: maxHeight, 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.below(maxHeight + 1);
}
});
result.total.should.be.below(totalTxCount);
done();
});
});
it('list with minConfirms', function (done) {
const minConfirms = 100000;
const options = { minConfirms: minConfirms, 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) {
transaction.pending.should.eql(false);
});
result.total.should.be.below(totalTxCount);
done();
});
});
it('list with limit and skip', function (done) {
const skipNum = 2;
const 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) {
const 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 () {
let keychain;
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 });
const infos = tx.receivedTravelInfo;
infos.should.have.length(2);
let 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 } });
});
const recipients = {};
recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 1e8;
assert.throws(function () {
new TransactionBuilder.createTransaction({ wallet: {}, recipients: [recipients] });
});
});
it('minConfirms argument', function () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 1e8;
assert.throws(function () {
new TransactionBuilder.createTransaction({
wallet: {},
recipients: recipients,
fee: 0,
minConfirms: 'string',
});
});
});
it('fee', function () {
const 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 () {
const 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 () {
const 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('spend from wallet with no unspents', function () {
let wallet;
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;
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 1e8; // wallet is empty
return TransactionBuilder.createTransaction({ wallet: wallet, recipients: recipients });
})
.catch(function (e) {
e.message.should.eql('no unspents available on wallet');
return wallet.delete({});
});
});
it('conflicting output script and address', function () {
const recipients: any[] = [];
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.containEql('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.
const 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 () {
const 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('uses all unspents passed into method', function () {
// Attempt to spend the full balance without any fees.
const 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
const sortedUnspents = _.reverse(_.sortBy(unspents.unspents, 'value'));
// limit the amount to no more than 15 unspents
const 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 () {
const 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 () {
const recipients: any[] = [];
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.txInfo.nP2pkhInputs.should.equal(15);
result.should.have.property('fee');
result.should.have.property('feeRate');
result.walletId.should.equal(wallet1.id());
});
});
it('Filter uneconomic unspents test, no feerate set', function () {
// prepare the mock
const walletmock = Object.create(wallet1);
let countLowInputs = 0;
return wallet1
.unspentsPaged(arguments)
.then(function (unspents) {
// limit the amount to no more than 15 unspents
const filteredArray = _.take(unspents.unspents, 15);
unspents.count = filteredArray.length;
unspents.unspents = filteredArray;
// mock a very low unspent value
unspents.unspents[2].value = 10;
walletmock.wallet.balance = _.sumBy(filteredArray, 'value');
for (let i = 0; i < unspents.count; i++) {
// count the number of inputs that are below 1 sat/Byte
if (unspents.unspents[i].value <= (1000 * VirtualSizes.txP2shInputSize) / 1000) {
countLowInputs++;
}
}
countLowInputs.should.be.above(0);
// mock the unspentsPaged call to return an unspent with a very low value
walletmock.unspentsPaged = function () {
return Q.fcall(function () {
should.equal(unspents.count, 15);
return unspents;
});
};
})
.then(function () {
const recipients = {};
// Spend the complete wallet balance minus some for fees.
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = walletmock.balance() - 120000;
return TransactionBuilder.createTransaction({
wallet: walletmock,
recipients: recipients,
minConfirms: 1,
});
})
.then(function (result) {
// several inputs are below fee cost to add them and should be pruned
result.txInfo.nP2shInputs.should.equal(15 - 1);
});
});
it('Filter uneconomic unspents test, given feerate', function () {
// prepare the mock
const walletmock = Object.create(wallet1);
let countLowInputs = 0;
return wallet1
.unspentsPaged(arguments)
.then(function (unspents) {
// limit the amount to no more than 15 unspents
const filteredArray = _.take(unspents.unspents, 15);
unspents.count = filteredArray.length;
unspents.unspents = filteredArray;
// mock a very low unspent value
unspents.unspents[2].value = 10;
walletmock.wallet.balance = _.sumBy(filteredArray, 'value');
for (let i = 0; i < unspents.count; i++) {
if (unspents.unspents[i].value <= (1000 * VirtualSizes.txP2shInputSize) / 1000) {
countLowInputs++;
}
}
countLowInputs.should.be.above(0);
walletmock.unspentsPaged = function () {
return Q.fcall(function () {
should.equal(unspents.count, 15);
return unspents;
});
};
})
.then(function () {
const recipients = {};
// Spend the complete wallet balance minus some for fees.
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = walletmock.balance() - 120000;
return TransactionBuilder.createTransaction({
wallet: walletmock,
recipients: recipients,
feeRate: 1000,
minConfirms: 1,
});
})
.then(function (result) {
// several inputs are below fee cost to add them and should be pruned
result.txInfo.nP2shInputs.should.equal(15 - 1);
});
});
it('no change required', function () {
// Attempt to spend the full balance without any fees.
const 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
const sortedUnspents = _.reverse(_.sortBy(unspents.unspents, 'value'));
// limit the amount to no more than 15 unspents
const 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 () {
const 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 () {
const recipients: any[] = [];
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 () {
let patch;
let patch2;
let patch3;
let patch4;
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) {
const 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) {
const 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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 10000 * 1e8;
return TransactionBuilder.createTransaction({ wallet: wallet1, recipients: recipients }).catch(function (e) {
e.message.should.containEql('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 () {
const recipients: any[] = [];
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 () {
const recipients: any[] = [];
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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
return TransactionBuilder.createTransaction({
wallet: wallet1,
recipients: recipients,
noSplitChange: true,
}).then(function (result) {
// 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 () {
const 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) {
const 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, 1506740);
});
});
it('do not override', function () {
const manualFee = 0.04 * 1e8;
const 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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
return TransactionBuilder.createTransaction({
wallet: wallet1,
recipients: recipients,
feeTxConfirmTarget: 1,
noSplitChange: true,
}).then(function (result) {
const 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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
return TransactionBuilder.createTransaction({
wallet: wallet1,
recipients: recipients,
feeTxConfirmTarget: 1,
maxFeeRate: 5000,
noSplitChange: true,
}).then(function (result) {
const feeUsed = result.fee;
assert.equal(feeUsed, 376635);
});
});
it('approximate with feeRate set by feeTxConfirmTarget 3 (estimatefee monkeypatch)', function () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
return TransactionBuilder.createTransaction({
wallet: wallet1,
recipients: recipients,
feeTxConfirmTarget: 3,
noSplitChange: true,
}).then(function (result) {
const feeUsed = result.fee;
assert.equal(feeUsed, 235021); // tx size will be 75kb * 0.0000312 * 1e8
});
});
it('approximate with feeRate with maxFeeRate (real service)', function () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
// undo the monkey patch so we get the right max fee
const 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;
const feeUsed = result.fee;
assert.equal(feeUsed, 165720);
});
});
it('approximate with feeRate set by feeTxConfirmTarget fallback (estimatefee monkeypatch)', function () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6200 * 1e8;
return TransactionBuilder.createTransaction({
wallet: wallet1,
recipients: recipients,
feeTxConfirmTarget: 4,
noSplitChange: true,
}).then(function (result) {
const feeUsed = result.fee;
assert.equal(feeUsed, 75327000); // tx size will be 75kb * 0.01 (max feerate as defined in bitgo.js)
});
});
it('validate (disable address verification)', function () {
const manualFee = 0.04 * 1e8;
const recipients = {};
recipients[TestBitGo.TEST_WALLET2_ADDRESS] = 6194e8;
const 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 () {
const 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 () {
const 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 () {
const 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 () {
const 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 () {
const recipients: any[] = [];
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 () {
const recipients: any[] = [];
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 () {
const recipients: any[] = [];
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 () {
const recipients: any[] = [];
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
const transaction = bitcoin.bitgo.createTransactionFromHex(result.transactionHex, utxolib.networks.bitcoin);
const singleKeyInput = transaction.ins[transaction.ins.length - 1];
const inputTxHash = Buffer.from(singleKeyInput.hash).reverse().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) {
const inputTx = response.body;
const output = inputTx.outputs[singleKeyInput.index];
const feeAddressInputValue = output.value;
const feeAddressChangeAddress = _.find(result.changeAddresses, {
address: 'mibJ4uJc9f1fbMeaUXNuWqsB1JgNMcTZK7',
});
const feeAddressChangeAmount = (feeAddressChangeAddress as any).amount;
// calculate the implied fee by using the input amount minus the output and ensure this amount was the final fee for the tx
const impliedFeeFromTx = feeAddressInputValue - feeAddressChangeAmount;
impliedFeeFromTx.should.eql(result.fee);
});
});
});
it('ok with single fee wallet address and key', function () {
const recipients: any[] = [];
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 () {
let unsignedTransaction;
let unsignedTransactionUsingSingleKeyFeeAddress;
let keychain;
before(function (done) {
bitgo.unlock({ otp: bitgo.testUserOTP() }, function (err) {
assert.equal(err, null);
// Go fetch the private key for our keychain
const options = {
xpub: wallet1.keychains[0].xpub,
};
bitgo.keychains().get(options, function (err, result) {
assert.equal(err, null);
keychain = result;
const 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 () {
const 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) {
const 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.
const 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.
const realVerifyInputSignatures = TransactionBuilder.verifyInputSignatures;
let 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) {
const 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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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 () {
const seqId = Math.floor(Math.random() * 1e16).toString(16);
let 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 () {
let 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 () {
const 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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('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.containEql('invalid bitcoin address');
return wallet1.sendMany({
recipients: [{ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: -100 }],
walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE,
});
})
.then(function () {
throw new Error('Unexpected result - expected to catch bad amount');
})
.catch(function (err) {
err.message.should.containEql('invalid amount');
// use a ridiculously high number for the minConfirms so that no viable unspents are returned
return bitgo.unlock({ otp: '0000000' });
})
.then(function () {
return wallet1.sendMany({
recipients: [{ address: TestBitGo.TEST_WALLET2_ADDRESS, amount: 10000 }],
walletPassphrase: TestBitGo.TEST_WALLET1_PASSCODE,
enforceMinConfirmsForChange: true,
minConfirms: 999999999,
});
})
.then(function () {
throw new Error('Unexpected result - expected to catch 0 unspents');
})
.catch(function (err) {
err.message.should.containEql('0 unspents available for transaction creation');
});
});
describe('Bad input', function () {
it('send many - insufficient funds', function (done) {
const 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) {
const 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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.0009 * 1e8;
return safewallet
.createTransaction({ recipients: recipients })
.then(function (tx) {
const enc =
'{"iv":"lFkIIulsbL+Ub2jGUiXdrw==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"pdx6d0iD+Io=","ct":"kVIZBeHxoxt19ki0hs5WBjmuLdHPfBQ30a0iGb5H+pR6+kH5lr3zxPL0xeO5EtwPRR0Mw0JVuLqapQE="}';
const 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) {
const 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 () {
const recipients: any[] = [];
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 () {
const recipients: any[] = [];
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) {
const recipients: any[] = [];
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 () {
const recipients: any[] = [];
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);
let 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 () {
const 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 () {
let keychain;
let tx;
before(function (done) {
// Set up keychain
const 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 () {
const recipients: any[] = [];
recipients.push({
address: TestBitGo.TEST_WALLET2_ADDRESS,
amount: 0.001 * 1e8,
});
let calledVerify = false;
let setValidate = false;
const realVerifyInputSignatures = TransactionBuilder.verifyInputSignatures;
TransactionBuilder.verifyInputSignatures = function () {
calledVerify = true;
return -1;
};
const 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 () {
const recipients: any[] = [];
recipients.push({
address: TestBitGo.TEST_WALLET2_ADDRESS,
amount: 0.001 * 1e8,
travelInfo: {
fromUserName: 42,
},
});
const wallet = Object.create(wallet1);
return wallet
.createTransaction({ recipients: recipients })
.then(function (result) {
// should not reach
assert(false);
})
.catch(function (err) {
err.message.should.containEql('incorrect type for field fromUserName in travel info');
});
});
it('create tx with travelInfo', function () {
const recipients: any[] = [];
recipients.push({
address: TestBitGo.TEST_WALLET2_ADDRESS,
amount: 0.001 * 1e8,
travelInfo: {
fromUserName: 'Alice',
toUserName: 'Bob',
},
});
const wallet = Object.create(wallet1);
return wallet.createTransaction({ recipients: recipients }).then(function (res) {
res.should.have.property('travelInfos');
res.travelInfos.should.have.length(1);
const 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) {
const 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) {
const 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 () {
const recipients = {};
let 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) {
return bitgo.unlock({ otp: '0000000' }).then(function () {
wallet1.sendTransaction({ tx: tx }, function (err, result) {
assert.equal(err, null);
result.should.have.property('tx');
result.should.have.property('hash');
done();
});
});
});
});
describe('CPFP', function () {
let parentTxHash;
let defaultFee;
before(function () {
return bitgo
.unlock({ otp: '0000000' })
.then(function () {
// broadcast parent tx
const recipients = {};
recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.001 * 1e8;
return wallet3.sendMany({
recipients: recipients,
walletPassphrase: TestBitGo.TEST_WALLET3_PASSCODE,
fee: 10000, // extremely low fee
});
})
.then(function (result) {
result.should.have.property('hash');
parentTxHash = result.hash;
})
.delay(3000); // give the indexer some time to pick up the tx
});
it('child should pay more than usual', function () {
return bitgo
.estimateFee({ numBlocks: 2, maxFee: 1.0 * 1e8, inputs: [parentTxHash], txSize: 300, cpfpAware: true })
.then(function (result) {
result.should.have.property('feePerKb');
defaultFee = result.feePerKb;
result.should.have.property('cpfpFeePerKb');
defaultFee.should.be.lessThan(result.cpfpFeePerKb);
});
});
it('child fee capped by maxFee', function () {
const maxFee = defaultFee + 1000;
return bitgo
.estimateFee({ numBlocks: 2, maxFee: maxFee, inputs: [parentTxHash], txSize: 300, cpfpAware: true })
.then(function (result) {
result.should.have.property('feePerKb');
result.should.have.property('cpfpFeePerKb');
result.cpfpFeePerKb.should.equal(maxFee);
});
});
it('disable cpfp awareness', function () {
return bitgo
.estimateFee({ numBlocks: 2, maxFee: 1.0 * 1e8, inputs: [parentTxHash], txSize: 300, cpfpAware: false })
.then(function (result) {
result.should.have.property('feePerKb');
result.should.have.property('cpfpFeePerKb');
result.feePerKb.should.equal(result.cpfpFeePerKb);
});
});
});
// Now send the money back
describe('return transaction', function () {
let keychain;
let tx;
it('keychain', function (done) {
const 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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.001 * 1e8;
// monkey patch unspents to check that expected options are sent
const 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 () {
const recipients = {};
recipients[TestBitGo.TEST_WALLET1_ADDRESS] = 0.001 * 1e8;
// monkey patch unspents to check that expected options are sent
const 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();
});
let 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());
const 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) {
const 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) {
const 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());
const 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) {
const 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.containEql('wallet is frozen');
done();
}
);
});
});
});
describe('Accelerate Transaction (test server)', function accelerateTxDescribe() {
const bitgo = new TestBitGo();
bitgo.initializeTestVars();
const userKeypair = {
xprv: 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k',
xpub: 'xpub661MyMwAqRbcF9Nc7TbBo1rZAagiWEVPWKbDKThNG8zqjk76HAKLkaSbTn6dK2dQPfuD7xjicxCZVWvj67fP5nQ9W7QURmoMVAX8m6jZsGp',
rawPub: '02c103ac74481874b5ef0f385d12725e4f14aedc9e00bc814ce96f47f62ce7adf2',
rawPrv: '936c5af3f8af81f75cdad1b08f29e7d9c01e598e2db2d7be18b9e5a8646e87c6',
path: 'm',
walletSubPath: '/0/0',
};
const backupKeypair = {
xprv: 'xprv9s21ZrQH143K47sEkLkykgYmq1xF5ZWrPYhUZcmBpPFMQojvGUmEcr5jFXYGfr8CpFdpTvhQ7L9NN2rLtsBFjSix3BAjwJcBj6U3D5hxTPc',
xpub: 'xpub661MyMwAqRbcGbwhrNHz7pVWP3njV2Ehkmd5N1AoNinLHc54p25VAeQD6q2oTS3uuDMDnfnXnthbS9ufC8JVYpNnWU5Rn3pYaNuLCNywkw1',
rawPub: '03bbcb73997977068d9e36666bbd5cd37579acae8e2bd5ce9d0a6e5c150a423bc3',
rawPrv: '77a15f14796f4001d1092ae84f766bd869e9bee6bffae6547def5045b96fa943',
path: 'm',
walletSubPath: '/0/0',
};
const bitgoKey = {
xpub: 'xpub661MyMwAqRbcGQcVFiwcrtc7c3vopsX96jsJUYPcFMREcRTqAqsqbv2ZRyCJAPLm5NMHCy85E3ZwpT4EAUw9WGU7vMhG6z83hDeKXBWn6Lf',
path: 'm',
walletSubPath: '/0/0',
};
const wallet = new Wallet(bitgo, {
id: '2NCoSfHH6Ls4CdTS5QahgC9k7x9RfXeSwY4',
private: { keychains: [userKeypair, backupKeypair, bitgoKey] },
});
const minWalletBalance = 100000;
// TODO: temporarily use hard coded fee rate limits due to race condition
// in getConstants() causing max fee and min fee rates to change
// behind our backs once the response returns from the server.
// Additionally, the server lies to the client about the minimum fee
// rate it enforces, causing send failures when tx's are sent with a fee
// rate below the server's minimum rate, which is not exposed to the client
// const minFeeRate = bitgo.getConstants().minFeeRate;
// const maxFeeRate = bitgo.getConstants().maxFeeRate;
const minFeeRate = 2000;
const maxFeeRate = 10100;
let parentTx;
let parentTxFeeRate;
before(async function coAccelerateTxBefore() {
require('nock').restore();
if (bitgo._token === undefined || bitgo._token === null) {
await bitgo.authenticateTestUser(bitgo.testUserOTP());
}
await bitgo.unlock({ otp: bitgo.testUserOTP() });
await wallet.get();
const walletBalance = wallet.balance();
if (walletBalance < minWalletBalance) {
// ask to fund wallet if there are less than 100k satoshi
throw new Error(
`The v1 TBTC Test Wallet ${wallet.id()} doesn't have enough funds to run the test suite. The current balance is ${walletBalance} and the minimum balance is ${minWalletBalance}. Please send at least ${
minWalletBalance - walletBalance
} TBTC to that address`
);
}
// random low fee rate at most 10% above the min fee rate, with a minimum of 500 sat / 1000 bytes.
// Maximum must also be bounded by the maxFeeRate, which the parent tx cannot break
const randomFeeRate = Math.max(Math.floor(Math.random() * minFeeRate * 0.1) + minFeeRate, 500);
parentTxFeeRate = Math.min(Math.max(randomFeeRate, minFeeRate), maxFeeRate);
// create stuck parent tx
const address = await wallet.createAddress();
parentTx = await wallet.createAndSignTransaction({
recipients: [
{
address: address.address,
amount: 10000,
},
],
feeRate: parentTxFeeRate,
xprv: wallet.keychains[0].xprv,
});
const sendResult = await wallet.sendTransaction(parentTx);
parentTx = _.merge(parentTx, _.pick(sendResult, ['hash']));
// allow parent tx time to be indexed by smartbit
return new Promise((resolve) => setTimeout(resolve, 10000));
});
async function verifyTargetFeeRate({ parentTx, childTx, targetRate }) {
const explorerBaseUrl = common.Environments[bitgo.getEnv()].btcExplorerBaseUrl;
const getTx = async (txId) => await request.get(`${explorerBaseUrl}/tx/${txId}`).send();
const parent = await getTx(parentTx.hash);
const child = await getTx(childTx.hash);
const childFee = child.body.fee;
const childVSize = Math.ceil(child.body.weight / 4);
const parentFee = parent.body.fee;
const parentVSize = Math.ceil(parent.body.weight / 4);
const combinedVSize = childVSize + parentVSize;
const combinedFee = childFee + parentFee;
const combinedRate = (1000 * combinedFee) / combinedVSize;
// ensure the actual combined rate is within 2% of the target rate.
// This inexact fee rate result is usually due to the child tx witness
// signatures being one byte less than was estimated when creating
// the child transaction. It is also possible to create a valid
// signature which is more than one byte less than was estimated, but
// this case should be sufficiently rare that it is not handled in this test.
const tolerance = 0.02 * targetRate;
combinedRate.should.be.within(targetRate - tolerance, targetRate + tolerance);
}
it('accelerates a stuck tx', async function () {
// random fee rate at least 10% above the parentTxFeeRate,
// but no more than the max fee rate
const minCombinedTxFeeRate = parentTxFeeRate * 1.1;
const combinedTxFeeRate = Math.floor(Math.random() * (maxFeeRate - minCombinedTxFeeRate) + minCombinedTxFeeRate);
const childTx = await wallet.accelerateTransaction({
transactionID: parentTx.hash,
feeRate: combinedTxFeeRate,
xprv: userKeypair.xprv,
});
// verify childTx
should.exist(childTx);
childTx.should.have.property('status', 'accepted');
// allow child tx time to be indexed by smartbit
await new Promise((resolve) => setTimeout(resolve, 10000));
return verifyTargetFeeRate({ parentTx, childTx, targetRate: combinedTxFeeRate });
});
});
Выполнить команду
Для локальной разработки. Не используйте в интернете!