@ducatus/ducatus-wallet-client-rev
Version:
Client for @ducatus/ducatus-wallet-service-rev
1,562 lines (1,394 loc) • 239 kB
JavaScript
'use strict';
var _ = require('lodash');
var $ = require('preconditions').singleton();
var chai = require('chai');
chai.config.includeStack = true;
var sinon = require('sinon');
var should = chai.should();
var async = require('async');
var request = require('supertest');
var Uuid = require('uuid');
var sjcl = require('sjcl');
var log = require('../ts_build/lib/log');
var mongodb = require('mongodb');
var config = require('./test-config');
var oldCredentials = require('./legacyCredentialsExports');
var CWC = require('../../ducatus-crypto-wallet-core-rev');
var Bitcore = CWC.BitcoreLib;
var Bitcore_ = {
btc: Bitcore,
bch: CWC.BitcoreLibCash,
};
var BWS = require('@ducatus/ducatus-wallet-service-rev');
var { Constants } = require('../ts_build/lib/common');
var Client = require('../ts_build').default;
var Key = Client.Key;
var { Request } = require('../ts_build/lib/request.js');
var { Utils } = require('../ts_build/lib/common');
var ExpressApp = BWS.ExpressApp;
var Storage = BWS.Storage;
var TestData = require('./testdata');
var Errors = require('../ts_build/lib/errors');
var helpers = {};
helpers.toSatoshi = (btc) => {
if (_.isArray(btc)) {
return _.map(btc, helpers.toSatoshi);
} else {
return parseFloat((btc * 1e8).toPrecision(12));
}
};
helpers.newClient = (app) => {
$.checkArgument(app);
return new Client({
baseUrl: '/bws/api',
request: request(app),
bp_partner: 'xxx',
bp_partner_version: 'yyy',
// logLevel: 'debug',
});
};
helpers.stubRequest = (err, res) => {
var request = {
accept: sinon.stub(),
set: sinon.stub(),
query: sinon.stub(),
send: sinon.stub(),
timeout: sinon.stub(),
end: sinon.stub().yields(err, res),
};
var reqFactory = _.reduce(['get', 'post', 'put', 'delete'], (mem, verb) => {
mem[verb] = (url) => {
return request;
};
return mem;
}, {});
return reqFactory;
};
helpers.generateUtxos = (scriptType, publicKeyRing, path, requiredSignatures, amounts) => {
var amounts = [].concat(amounts);
var utxos = _.map(amounts, (amount, i) => {
var address = Utils.deriveAddress(scriptType, publicKeyRing, path, requiredSignatures, 'testnet');
var scriptPubKey;
switch (scriptType) {
case Constants.SCRIPT_TYPES.P2WSH:
case Constants.SCRIPT_TYPES.P2SH:
scriptPubKey = new Bitcore.Script.buildMultisigOut(address.publicKeys, requiredSignatures).toScriptHashOut();
break;
case Constants.SCRIPT_TYPES.P2WPKH:
case Constants.SCRIPT_TYPES.P2PKH:
scriptPubKey = new Bitcore.Script.buildPublicKeyHashOut(address.address);
break;
}
should.exist(scriptPubKey);
var obj = {
txid: new Bitcore.crypto.Hash.sha256(Buffer.alloc(i)).toString('hex'),
vout: 100,
satoshis: helpers.toSatoshi(amount),
scriptPubKey: scriptPubKey.toBuffer().toString('hex'),
address: address.address,
path: path,
publicKeys: address.publicKeys,
};
return obj;
});
return utxos;
};
helpers.createAndJoinWallet = (clients, keys, m, n, opts, cb) => {
opts = opts || {};
var coin = opts.coin || 'btc';
var network = opts.network || 'testnet';
let keyOpts = {
useLegacyCoinType: opts.useLegacyCoinType,
useLegacyPurpose: opts.useLegacyPurpose,
passphrase: opts.passphrase,
};
keys[0] = opts.key || Key.create(keyOpts);
let cred = keys[0].createCredentials(null, { coin: coin, network: network, account: 0, n: n, addressType: opts.addressType });
clients[0].fromObj(cred);
clients[0].createWallet('mywallet', 'creator', m, n, {
coin: coin,
network: network,
singleAddress: !!opts.singleAddress,
doNotCheck: true,
useNativeSegwit: !!opts.useNativeSegwit
}, (err, secret) => {
if (err) console.log(err);
should.not.exist(err);
if (n > 1) {
should.exist(secret);
}
async.series([
(next) => {
async.each(_.range(1, n), (i, cb) => {
keys[i] = Key.create(keyOpts);
clients[i].fromString(
keys[i].createCredentials(null, {
coin: coin,
network: network,
account: 0,
n: n,
addressType: opts.addressType
})
);
clients[i].joinWallet(secret, 'copayer ' + i, {
coin: coin
}, cb);
}, next);
},
(next) => {
async.each(_.range(n), (i, cb) => {
clients[i].openWallet(cb);
}, next);
},
],
(err) => {
should.not.exist(err);
return cb({
m: m,
n: n,
secret: secret,
});
});
});
};
helpers.tamperResponse = (clients, method, url, args, tamper, cb) => {
clients = [].concat(clients);
// Use first client to get a clean response from server
clients[0].request.doRequest(method, url, args, false, (err, result) => {
should.not.exist(err);
tamper(result);
// Return tampered data for every client in the list
_.each(clients, (client) => {
client.request.doRequest = sinon.stub().withArgs(method, url).yields(null, result);
});
return cb();
});
};
helpers.createAndPublishTxProposal = (client, opts, cb) => {
if (!opts.outputs) {
opts.outputs = [{
toAddress: opts.toAddress,
amount: opts.amount,
}];
}
client.createTxProposal(opts, (err, txp) => {
if (err) return cb(err);
client.publishTxProposal({
txp: txp
}, cb);
});
};
var blockchainExplorerMock = {
register: sinon.stub().callsArgWith(1, null, null),
getCheckData: sinon.stub().callsArgWith(1, null, { sum: 100 }),
addAddresses: sinon.stub().callsArgWith(2, null, null),
};
blockchainExplorerMock.getUtxos = (wallet, height, cb) => {
return cb(null, _.cloneDeep(blockchainExplorerMock.utxos));
};
// v8
blockchainExplorerMock.getAddressUtxos = (address, height, cb) => {
var selected = _.filter(blockchainExplorerMock.utxos, (utxo) => {
return _.includes(address, utxo.address);
});
return cb(null, _.cloneDeep(selected));
};
blockchainExplorerMock.setUtxo = (address, amount, m, confirmations) => {
var B = Bitcore_[address.coin];
var scriptPubKey;
switch (address.type) {
case Constants.SCRIPT_TYPES.P2WSH:
case Constants.SCRIPT_TYPES.P2SH:
scriptPubKey = address.publicKeys ? B.Script.buildMultisigOut(address.publicKeys, m).toScriptHashOut() : '';
break;
case Constants.SCRIPT_TYPES.P2WPKH:
case Constants.SCRIPT_TYPES.P2PKH:
scriptPubKey = B.Script.buildPublicKeyHashOut(address.address);
break;
}
should.exist(scriptPubKey);
blockchainExplorerMock.utxos.push({
txid: new Bitcore.crypto.Hash.sha256(Buffer.alloc(Math.random() * 100000)).toString('hex'),
outputIndex: 0,
amount: amount,
satoshis: amount * 1e8,
address: address.address,
scriptPubKey: scriptPubKey.toBuffer().toString('hex'),
confirmations: _.isUndefined(confirmations) ? Math.floor((Math.random() * 100) + 1) : +confirmations,
});
};
blockchainExplorerMock.supportsGrouping = () => { return false; }
blockchainExplorerMock.getBlockchainHeight = (cb) => { return cb(null, 1000); }
blockchainExplorerMock.broadcast = (raw, cb) => {
blockchainExplorerMock.lastBroadcasted = raw;
let hash;
try {
let tx = new Bitcore.Transaction(raw);
if (_.isEmpty(tx.outputs)) {
throw 'no bitcoin';
}
hash = tx.id;
// btc/bch
return cb(null, hash);
} catch (e) {
// try eth
hash = CWC.Transactions.getHash({
tx: raw[0],
chain: 'ETH',
});
return cb(null, hash);
};
};
blockchainExplorerMock.setHistory = (txs) => {
blockchainExplorerMock.txHistory = txs;
};
blockchainExplorerMock.getTransaction = (txid, cb) => {
return cb();
};
var createTxsV8 = (nr, bcHeight, txs) => {
txs = txs || [];
// Will generate
// order / confirmations / height / txid
// 0. => -1 / -1 / txid0 / id0 <= LAST ONE!
// 1. => 1 / bcHeight / txid1
// 2. => 2 / bcHeight - 1 / txid2
// 3. => 3... / bcHeight - 2 / txid3
var i = 0;
if (_.isEmpty(txs)) {
while (i < nr) {
txs.push({
id: 'id' + i,
txid: 'txid' + i,
size: 226,
category: 'receive',
satoshis: 30001,
// this is translated on V8.prototype.getTransactions
amount: 30001 / 1e8,
height: (i == 0) ? -1 : bcHeight - i + 1,
address: 'muFJi3ZPfR5nhxyD7dfpx2nYZA8Wmwzgck',
blockTime: '2018-09-21T18:08:31.000Z',
});
i++;
}
}
return txs;
};
blockchainExplorerMock.getTransactions = (wallet, startBlock, cb) => {
var list = [].concat(blockchainExplorerMock.txHistory);
// -1 = mempool, always included in server' s v8.js
list = _.filter(list, (x) => { return x.height >= startBlock || x.height == -1; });
return cb(null, list);
};
blockchainExplorerMock.getAddressActivity = (address, cb) => {
var activeAddresses = _.map(blockchainExplorerMock.utxos || [], 'address');
return cb(null, _.includes(activeAddresses, address));
};
blockchainExplorerMock.setFeeLevels = (levels) => {
blockchainExplorerMock.feeLevels = levels;
};
blockchainExplorerMock.estimateFee = (nbBlocks, cb) => {
var levels = {};
_.each(nbBlocks, (nb) => {
var feePerKb = blockchainExplorerMock.feeLevels[nb];
levels[nb] = _.isNumber(feePerKb) ? feePerKb / 1e8 : -1;
});
return cb(null, levels);
};
blockchainExplorerMock.estimateGas = (nbBlocks, cb) => {
return cb(null, '20000000000');
};
blockchainExplorerMock.getBalance = (nbBlocks, cb) => {
return cb(null, {
unconfirmed: 0,
confirmed: 20000000000 * 5,
balance: 20000000000 * 5,
});
};
blockchainExplorerMock.getTransactionCount = (addr, cb) => {
return cb(null, 0);
};
blockchainExplorerMock.reset = () => {
blockchainExplorerMock.utxos = [];
blockchainExplorerMock.txHistory = [];
blockchainExplorerMock.feeLevels = [];
};
helpers.newDb = (extra, cb) => {
extra = extra || '';
mongodb.MongoClient.connect(config.mongoDb.uri + extra, (err, in_db) => {
if (err) return cb(err);
in_db.dropDatabase((err) => {
return cb(err, in_db);
});
});
}
var db;
describe('client API', function() { // DONT USE LAMBAS HERE!!! https://stackoverflow.com/questions/23492043/change-default-timeout-for-mocha, or this.timeout() will BREAK!
//
var clients, app, sandbox, storage, keys, i;
this.timeout(8000);
before((done) => {
i = 0;
clients = [];
keys = [];
helpers.newDb('', (err, in_db) => {
db = in_db;
storage = new Storage({
db: db,
});
Storage.createIndexes(db);
return done(err);
});
});
beforeEach((done) => {
var expressApp = new ExpressApp();
expressApp.start({
ignoreRateLimiter: true,
storage: storage,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
doNotCheckV8: true,
},
() => {
app = expressApp.app;
// Generates 5 clients
clients = _.map(_.range(5), (i) => {
return helpers.newClient(app);
});
blockchainExplorerMock.reset();
sandbox = sinon.createSandbox();
if (!process.env.BWC_SHOW_LOGS) {
sandbox.stub(log, 'warn');
sandbox.stub(log, 'info');
sandbox.stub(log, 'error');
}
done();
});
});
afterEach((done) => {
sandbox.restore();
done();
});
describe('constructor', () => {
it('should set the log level based on the logLevel option', () => {
var originalLogLevel = log.level;
var client = new Client({
logLevel: 'info'
});
client.logLevel.should.equal('info');
log.level.should.equal('info');
var client = new Client({
logLevel: 'debug'
});
client.logLevel.should.equal('debug');
log.level.should.equal('debug');
log.level = originalLogLevel; //restore since log is a singleton
});
it('should use silent for the log level if no logLevel is specified', () => {
var originalLogLevel = log.level;
log.level = 'foo;'
var client = new Client();
client.logLevel.should.equal('silent');
log.level.should.equal('silent');
log.level = originalLogLevel; //restore since log is a singleton
});
});
describe('Client Internals', () => {
it('should expose bitcore', () => {
should.exist(Bitcore);
should.exist(Bitcore.HDPublicKey);
});
});
// todo
describe('Server internals', () => {
var k;
before(() => {
k = Key.create();
});
it('should allow cors', (done) => {
clients[0].credentials = {};
clients[0].request.doRequest('options', '/', {}, false, (err, x, headers) => {
headers['access-control-allow-origin'].should.equal('*');
should.exist(headers['access-control-allow-methods']);
should.exist(headers['access-control-allow-headers']);
done();
});
});
it('should request set credentials before creating/joining', (done) => {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields('bigerror');
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, () => {
var client = helpers.newClient(app);
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
(err) => {
should.exist(err);
err.toString().should.contain('credentials');
done();
})
});
});
it('should handle critical errors', (done) => {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields('bigerror');
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, () => {
var s2 = sinon.stub();
s2.load = sinon.stub().yields(null);
var client = helpers.newClient(app);
client.storage = s2;
client.fromString(
k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 })
);
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
(err) => {
err.should.be.an.instanceOf(Error);
err.message.should.equal('bigerror');
done();
});
});
});
it('should handle critical errors (Case2)', (done) => {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields({
code: 501,
message: 'wow'
});
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, () => {
var s2 = sinon.stub();
s2.load = sinon.stub().yields(null);
var client = helpers.newClient(app);
client.storage = s2;
client.fromString(
k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 })
);
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
(err) => {
err.should.be.an.instanceOf(Error);
err.message.should.equal('wow');
done();
});
});
});
it('should handle critical errors (Case3)', (done) => {
var s = sinon.stub();
s.storeWallet = sinon.stub().yields({
code: 404,
message: 'wow'
});
s.fetchWallet = sinon.stub().yields(null);
var expressApp = new ExpressApp();
expressApp.start({
storage: s,
blockchainExplorer: blockchainExplorerMock,
disableLogs: true,
}, () => {
var s2 = sinon.stub();
s2.load = sinon.stub().yields(null);
var client = helpers.newClient(app);
client.storage = s2;
client.fromString(
k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 })
);
client.createWallet('1', '2', 1, 1, {
network: 'testnet'
},
(err) => {
err.should.be.an.instanceOf(Errors.NOT_FOUND);
done();
});
});
});
it('should handle critical errors (Case4)', (done) => {
var body = {
code: 999,
message: 'unexpected body'
};
var ret = Request._parseError(body);
ret.should.be.an.instanceOf(Error);
ret.message.should.equal('999: unexpected body');
done();
});
it('should handle critical errors (Case5)', (done) => {
clients[0].request.r = helpers.stubRequest('some error');
clients[0].fromString(
k.createCredentials(null, { coin: 'btc', n: 1, network: 'testnet', account: 0 })
);
clients[0].createWallet('mywallet', 'creator', 1, 2, {
network: 'testnet'
}, (err, secret) => {
should.exist(err);
err.should.be.an.instanceOf(Errors.CONNECTION_ERROR);
done();
});
});
it('should correctly use remote message', (done) => {
var body = {
code: 'INSUFFICIENT_FUNDS',
};
var ret = Request._parseError(body);
ret.should.be.an.instanceOf(Error);
ret.message.should.equal('Insufficient funds.');
var body = {
code: 'INSUFFICIENT_FUNDS',
message: 'remote message',
};
var ret2 = Request._parseError(body);
ret2.should.be.an.instanceOf(Error);
ret2.message.should.equal('remote message');
var body = {
code: 'MADE_UP_ERROR',
message: 'remote message',
};
var ret3 = Request._parseError(body);
ret3.should.be.an.instanceOf(Error);
ret3.message.should.equal('MADE_UP_ERROR: remote message');
done();
});
});
describe('Build & sign txs', () => {
var masterPrivateKey = 'tprv8ZgxMBicQKsPd8U9aBBJ5J2v8XMwKwZvf8qcu2gLK5FRrsrPeSgkEcNHqKx4zwv6cP536m68q2UD7wVM24zdSCpaJRmpowaeJTeVMXL5v5k';
var derivedPrivateKey = {
'BIP44': new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild("m/44'/1'/0'").toString(),
'BIP45': new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild("m/45'").toString(),
'BIP48': new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild("m/48'/1'/0'").toString(),
};
describe('#buildTx', () => {
it('Raw tx roundtrip', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress,
amount: 1200,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Client.getRawTx(txp);
should.exist(t);
_.isString(t).should.be.true;
/^[\da-f]+$/.test(t).should.be.true;
var t2 = new Bitcore.Transaction(t);
t2.inputs.length.should.equal(2);
t2.outputs.length.should.equal(2);
t2.outputs[0].satoshis.should.equal(1200);
});
it('should build a tx correctly (BIP44)', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress,
amount: 1200,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
should.not.exist(bitcoreError);
t.getFee().should.equal(10050);
});
it('should build a P2WPKH tx correctly (BIP44)', () => {
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
const toAddress = Utils.deriveAddress('P2WPKH', publicKeyRing, 'm/0/0', 1, 'livenet', 'btc');
const changeAddress = Utils.deriveAddress('P2WPKH', publicKeyRing, 'm/0/1', 1, 'livenet', 'btc');
toAddress.address.should.equal('bc1qrshu7r9z9y22y3wrrghfmjrvn0xxasfl7qrmvf');
changeAddress.address.should.equal('bc1quhzpvcmllzm3kkf7jwsxdemgaec3dz2j0uuan0');
var utxos = helpers.generateUtxos('P2WPKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress.address,
amount: 1200,
changeAddress: {
address: changeAddress.address
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP44',
addressType: 'P2WPKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
should.not.exist(bitcoreError);
t.getFee().should.equal(10050);
});
it('should build a P2WSH tx correctly (BIP48)', () => {
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP48']),
}];
const toAddress = Utils.deriveAddress('P2WSH', publicKeyRing, 'm/0/0', 1, 'livenet', 'btc');
const changeAddress = Utils.deriveAddress('P2WSH', publicKeyRing, 'm/0/1', 1, 'livenet', 'btc');
toAddress.address.should.equal('bc1qxq4tyr7uhwprj4w8ayc8manv4t64g0hc74ka9w4qka0uygr7gplqqnlu24');
changeAddress.address.should.equal('bc1qk8q74mfp7mcldhvfu4azjyqnu7rnd0d9ghdnxkxye34utvp0fgvq50jl0v');
var utxos = helpers.generateUtxos('P2WSH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress.address,
amount: 1200,
changeAddress: {
address: changeAddress.address
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP44',
addressType: 'P2WSH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
should.not.exist(bitcoreError);
t.getFee().should.equal(10050);
});
it('should build a tx correctly (BIP48)', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP48']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: '2.0.0',
inputs: utxos,
toAddress: toAddress,
amount: 1200,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10050,
derivationStrategy: 'BIP48',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
disableSmallFees: true,
disableLargeFees: true,
});
should.not.exist(bitcoreError);
t.getFee().should.equal(10050);
});
it('should build an eth txp correctly', () => {
const toAddress = '0xa062a07a0a56beb2872b12f388f511d694626730';
const key = Key.fromExtendedPrivateKey(masterPrivateKey);
const path = 'm/44\'/60\'/0\'';
const publicKeyRing = [{
xPubKey:
new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild(path).toString(),
}];
const from = Utils.deriveAddress('P2PKH', publicKeyRing, 'm/0/0', 1, 'livenet', 'eth');
const txp = {
version: 3,
from: from.address,
coin: 'eth',
outputs: [{
toAddress: toAddress,
amount: 3896000000000000,
gasLimit: 21000,
message: 'first output'
}
],
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 420000000000000,
nonce: 6,
gasPrice: 20000000000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
amount: 3896000000000000
};
var t = Utils.buildTx(txp);
const rawTxp = t.uncheckedSerialize();
rawTxp.should.deep.equal(['0xeb068504a817c80082520894a062a07a0a56beb2872b12f388f511d694626730870dd764300b800080018080']);
});
it('should protect from creating excessive fee', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1, 2]);
var txp = {
inputs: utxos,
toAddress: toAddress,
amount: 1.5e8,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 1.2e8,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var x = Utils;
x.newBitcoreTransaction = () => {
return {
from: sinon.stub(),
to: sinon.stub(),
change: sinon.stub(),
outputs: [{
satoshis: 1000,
}],
fee: sinon.stub(),
}
};
(() => {
var t = x.buildTx(txp);
}).should.throw('Illegal State');
x.newBitcoreTransaction = x;
});
it('should build a tx with multiple outputs', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
it('should build a tx with provided output scripts', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [0.001]);
var txp = {
inputs: utxos,
type: 'external',
outputs: [{
"toAddress": "18433T2TSgajt9jWhcTBw4GoNREA6LpX3E",
"amount": 700,
"script": "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae"
}, {
"amount": 600,
"script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac"
}, {
"amount": 0,
"script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210"
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2, 3],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
t.outputs.length.should.equal(4);
t.outputs[0].script.toHex().should.equal(txp.outputs[0].script);
t.outputs[0].satoshis.should.equal(txp.outputs[0].amount);
t.outputs[1].script.toHex().should.equal(txp.outputs[1].script);
t.outputs[1].satoshis.should.equal(txp.outputs[1].amount);
t.outputs[2].script.toHex().should.equal(txp.outputs[2].script);
t.outputs[2].satoshis.should.equal(txp.outputs[2].amount);
var changeScript = Bitcore.Script.fromAddress(txp.changeAddress.address).toHex();
t.outputs[3].script.toHex().should.equal(changeScript);
});
it('should fail if provided output has no either toAddress or script', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [0.001]);
var txp = {
inputs: utxos,
type: 'external',
outputs: [{
"amount": 700,
}, {
"amount": 600,
"script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac"
}, {
"amount": 0,
"script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210"
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2, 3],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
(() => {
var t = Utils.buildTx(txp);
}).should.throw('Output should have either toAddress or script specified');
txp.outputs[0].toAddress = "18433T2TSgajt9jWhcTBw4GoNREA6LpX3E";
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
delete txp.outputs[0].toAddress;
txp.outputs[0].script = "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae";
t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
it('should build a v3 tx proposal', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: 3,
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
it('should build a v4 tx proposal', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: 4,
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var t = Utils.buildTx(txp);
var bitcoreError = t.getSerializationError({
disableIsFullySigned: true,
});
should.not.exist(bitcoreError);
});
});
describe('#pushSignatures', () => {
it('should sign BIP45 P2SH correctly', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP45']),
}];
var utxos = helpers.generateUtxos('P2SH', publicKeyRing, 'm/2147483647/0/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
coin: 'btc',
toAddress: toAddress,
amount: 1200,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10000,
derivationStrategy: 'BIP45',
addressType: 'P2SH',
};
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var path = 'm/45\'';
var signatures = key.sign(path, txp);
// This is a GOOD tests, since bitcore ONLY accept VALID signatures
signatures.length.should.be.equal(utxos.length);
});
it('should sign BIP44 P2PKH correctly', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
toAddress: toAddress,
coin: 'btc',
amount: 1200,
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var path = 'm/44\'/1\'/0\'';
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var signatures = key.sign(path, txp);
// This is a GOOD test, since bitcore ONLY accept VALID signatures
signatures.length.should.be.equal(utxos.length);
});
it('should sign multiple-outputs proposal correctly', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
inputs: utxos,
coin: 'btc',
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var path = 'm/44\'/1\'/0\'';
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var signatures = key.sign(path, txp);
signatures.length.should.be.equal(utxos.length);
});
it('should sign proposal with provided output scripts correctly', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [0.001]);
var txp = {
inputs: utxos,
type: 'external',
coin: 'btc',
outputs: [{
"amount": 700,
"script": "512103ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff210314a96cd6f5a20826070173fe5b7e9797f21fc8ca4a55bcb2d2bde99f55dd352352ae"
}, {
"amount": 600,
"script": "76a9144d5bd54809f846dc6b1a14cbdd0ac87a3c66f76688ac"
}, {
"amount": 0,
"script": "6a1e43430102fa9213bc243af03857d0f9165e971153586d3915201201201210"
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2, 3],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var path = 'm/44\'/1\'/0\'';
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var signatures = key.sign(path, txp);
signatures.length.should.be.equal(utxos.length);
});
it('should sign btc proposal correctly', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: 3,
coin: 'btc',
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var path = 'm/44\'/1\'/0\'';
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var signatures = key.sign(path, txp);
signatures.length.should.be.equal(utxos.length);
signatures[0].should.equal('3045022100cfacaf8e4c9782f33f717eba3162d44cf9f34d9768a3bcd66b7052eb0868a0880220015e930e1f7d9a8b6b9e54d1450556bf4ba95c2cf8ef5c55d97de7df270cc6fd');
signatures[1].should.equal('3044022069cf6e5d8700ff117f754e4183e81690d99d6a6443e86c9589efa072ecb7d82c02204c254506ac38774a2176f9ef56cc239ef7867fbd24da2bef795128c75a063301');
});
it('should sign btc proposal correctly (tx V2)', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: 4,
coin: 'btc',
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var path = 'm/44\'/1\'/0\'';
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var signatures = key.sign(path, txp);
signatures.length.should.be.equal(utxos.length);
signatures[0].should.equal('3045022100da83ffb02ce0c5c7f2b30d0eb2fd62d1177d282fff5ce7deb9d3a8fd6e002c9d022030f0f0b29dd1fb9b602c50e8916568aa0dd68054523989291decfdbf36d70299');
signatures[1].should.equal('3045022100951f980ad2fcd764a7824575e18aa4f28309b7160c353a0e3d239bff83050184022039c4ab5be5c40d19cd2c8bfcbf42a6262df851454a494ad78668be7d35519f05');
});
it('should sign eth proposal correctly', () => {
const toAddress = '0xa062a07a0a56beb2872b12f388f511d694626730';
const key = Key.fromExtendedPrivateKey(masterPrivateKey);
const path = 'm/44\'/60\'/0\'';
const publicKeyRing = [{
xPubKey:
new Bitcore.HDPrivateKey(masterPrivateKey).deriveChild(path).toString(),
}];
const from = Utils.deriveAddress('P2PKH', publicKeyRing, 'm/0/0', 1, 'livenet', 'eth');
const txp = {
version: 3,
from: from.address,
coin: 'eth',
outputs: [{
toAddress: toAddress,
amount: 3896000000000000,
gasLimit: 21000,
message: 'first output'
}
],
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 420000000000000,
nonce: 6,
gasPrice: 20000000000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
amount: 3896000000000000
};
const signatures = key.sign(path, txp);
const expectedSignatures = [
'0x4f761cd5f1cf1008d398c854ee338f82b457dc67ae794a987083b36b83fc6c917247fe72fe1880c0ee914c6e1b608625d8ab4e735520c33b2f7f76e0dcaf59801c',
];
signatures.should.deep.equal(expectedSignatures);
});
it('should sign BCH proposal correctly', () => {
var toAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var changeAddress = 'msj42CCGruhRsFrGATiUuh25dtxYtnpbTx';
var publicKeyRing = [{
xPubKey: new Bitcore.HDPublicKey(derivedPrivateKey['BIP44']),
}];
var utxos = helpers.generateUtxos('P2PKH', publicKeyRing, 'm/1/0', 1, [1000, 2000]);
var txp = {
version: 3,
coin: 'bch',
inputs: utxos,
outputs: [{
toAddress: toAddress,
amount: 800,
message: 'first output'
}, {
toAddress: toAddress,
amount: 900,
message: 'second output'
}],
changeAddress: {
address: changeAddress
},
requiredSignatures: 1,
outputOrder: [0, 1, 2],
fee: 10000,
derivationStrategy: 'BIP44',
addressType: 'P2PKH',
};
var path = 'm/44\'/1\'/0\'';
var key = Key.fromExtendedPrivateKey(masterPrivateKey);
var signatures = key.sign(path, txp);
signatures.length.should.be.equal(utxos.length);
signatures[0].should.equal('304402200aa70dfe99e25792c4a7edf773477100b6659f1ba906e551e6e5218ec32d273402202e31c575edb55b2da824e8cafd02b4769017ef63d3c888718cf6f0243c570d41');
signatures[1].should.equal('3045022100afde45e125f654453493b40d288cd66e8a011c66484509ae730a2686c9dff30502201bf34a6672c5848dd010b89ea1a5f040731acf78fec062f61b305e9ce32798a5');
});
});
});
describe('Wallet secret round trip', () => {
it('should create secret and parse secret', () => {
var i = 0;
while (i++ < 100) {
var walletId = Uuid.v4();
var walletPrivKey = new Bitcore.PrivateKey();
var network = i % 2 == 0 ? 'testnet' : 'livenet';
var coin = i % 3 == 0 ? 'bch' : 'btc';
var secret = Client._buildSecret(walletId, walletPrivKey, coin, network);
var result = Client.parseSecret(secret);
result.walletId.should.equal(walletId);
result.walletPrivKey.toString().should.equal(walletPrivKey.toString());
result.coin.should.equal(coin);
result.network.should.equal(network);
};
});
it('should fail on invalid secret', () => {
(() => {
Client.parseSecret('invalidSecret');
}).should.throw('Invalid secret');
});
it('should create secret and parse secret from string', () => {
var walletId = Uuid.v4();
var walletPrivKey = new Bitcore.PrivateKey();
var coin = 'btc';
var network = 'testnet';
var secret = Client._buildSecret(walletId, walletPrivKey.toString(), coin, network);
var result = Client.parseSecret(secret);
result.walletId.should.equal(walletId);
result.walletPrivKey.toString().should.equal(walletPrivKey.toString());
result.coin.should.equal(coin);
result.network.should.equal(network);
});
it('should default to btc for secrets not specifying coin', () => {
var result = Client.parseSecret('5ZN64RxKWCJXcy1pZwgrAzL1NnN5FQic5M2tLJVG5bEHaGXNRQs2uzJjMa9pMAbP5rz9Vu2xSaT');
result.coin.should.equal('btc');
});
});
describe('Notification polling', () => {
var clock, interval;
beforeEach(() => {
clock = sinon.useFakeTimers({ now: 1234000, toFake: ['Date'] });
});
afterEach(() => {
clock.restore();
});
it('should fetch notifications at intervals', (done) => {
helpers.createAndJoinWallet(clients, keys, 2, 2, {}, () => {
clients[0].on('notification', (data) => {
notifications.push(data);
});
var notifications = [];
clients[0]._fetchLatestNotifications(5, () => {
_.map(notifications, 'type').should.deep.equal(['NewCopayer', 'WalletComplete']);
clock.tick(2000);
notifications = [];
clients[0]._fetchLatestNotifications(5, () => {
notifications.length.should.equal(0);
clock.tick(2000);
clients[1].createAddress((err, x) => {
should.not.exist(err);
clients[0]._fetchLatestNotifications(5, () => {
_.map(notifications, 'type').should.deep.equal(['NewAddress']);
clock.tick(2000);
notifications = [];
clients[0]._fetchLatestNotifications(5, () => {
notifications.length.should.equal(0);
clients[1].createAddress((err, x) => {
should.not.exist(err);
clock.tick(60 * 1000);
clients[0]._fetchLatestNotifications(5, () => {
notifications.length.should.equal(0);
done();
});
});
});
});
});
});
});
});
});
});
describe('Wallet Creation', () => {
var k;
beforeEach((done) => {
k = Key.create();
db.dropDatabase((err) => {
return done(err);
});
});
it('should fail to create wallet in bogus device', (done) => {
clients[0].fromString(
k.createCredentials(null, {
coin: 'btc',
network: 'livenet',
account: 0,
n: 1,
})
);
clients[0].keyDerivationOk = false;
clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, (err, secret) => {
should.exist(err);
err.toString().should.contain('Cannot');
should.not.exist(secret);
done();
});
});
it('should encrypt wallet name', (done) => {
clients[0].fromString(
k.createCredentials(null, {
coin: 'btc',
network: 'livenet',
account: 0,
n: 1,
})
);
var spy = sinon.spy(clients[0].request, 'post');
clients[0].createWallet('mywallet', 'pepe', 1, 1, {}, (err, secret) => {
should.not.exist(err);
var url = spy.getCall(0).args[0];
var body = JSON.stringify(spy.getCall(0).args[1]);
url.should.contain('/wallets');
body.should.not.contain('mywallet');
clients[0].getStatus({}, (err, status) => {
should.not.exist(err);
status.wallet.name.should.equal('mywallet');
done();
})
});
});
it('should encrypt copayer name in wallet creation', (done) => {
clients[0].