commit
1d8360b79a
|
@ -14,26 +14,26 @@ var MESSAGE_SIGNING_PATH = "m/1/0";
|
|||
|
||||
function Copayer(opts) {
|
||||
opts = opts || {};
|
||||
opts.copayerIndex = opts.copayerIndex || 0;
|
||||
opts.copayerIndex = opts.copayerIndex || 0;
|
||||
Copayer.super_.apply(this, [opts]);
|
||||
|
||||
this.version = VERSION;
|
||||
this.createdOn = Math.floor(Date.now() / 1000);
|
||||
this.id = opts.id;
|
||||
this.name = opts.name;
|
||||
this.xPubKey = opts.xPubKey;
|
||||
this.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently
|
||||
this.createdOn = Math.floor(Date.now() / 1000);
|
||||
this.id = opts.id;
|
||||
this.name = opts.name;
|
||||
this.xPubKey = opts.xPubKey;
|
||||
this.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently
|
||||
this.signingPubKey = opts.signingPubKey || this.getSigningPubKey();
|
||||
};
|
||||
|
||||
util.inherits(Copayer, Addressable);
|
||||
|
||||
Copayer.prototype.getSigningPubKey = function () {
|
||||
Copayer.prototype.getSigningPubKey = function() {
|
||||
if (!this.xPubKey) return null;
|
||||
return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString();
|
||||
};
|
||||
|
||||
Copayer.fromObj = function (obj) {
|
||||
Copayer.fromObj = function(obj) {
|
||||
var x = new Copayer();
|
||||
|
||||
x.createdOn = obj.createdOn;
|
||||
|
|
|
@ -57,11 +57,11 @@ Wallet.getMaxRequiredCopayers = function(totalCopayers) {
|
|||
return Wallet.COPAYER_PAIR_LIMITS[totalCopayers];
|
||||
};
|
||||
|
||||
Wallet.verifyCopayerLimits = function (m, n) {
|
||||
Wallet.verifyCopayerLimits = function(m, n) {
|
||||
return (n >= 1 && n <= 12) && (m >= 1 && m <= Wallet.COPAYER_PAIR_LIMITS[n]);
|
||||
};
|
||||
|
||||
Wallet.fromObj = function (obj) {
|
||||
Wallet.fromObj = function(obj) {
|
||||
var x = new Wallet();
|
||||
|
||||
x.createdOn = obj.createdOn;
|
||||
|
@ -71,7 +71,7 @@ Wallet.fromObj = function (obj) {
|
|||
x.n = obj.n;
|
||||
x.status = obj.status;
|
||||
x.publicKeyRing = obj.publicKeyRing;
|
||||
x.copayers = _.map(obj.copayers, function (copayer) {
|
||||
x.copayers = _.map(obj.copayers, function(copayer) {
|
||||
return new Copayer(copayer);
|
||||
});
|
||||
x.pubKey = obj.pubKey;
|
||||
|
@ -81,26 +81,28 @@ Wallet.fromObj = function (obj) {
|
|||
return x;
|
||||
};
|
||||
|
||||
Wallet.prototype.addCopayer = function (copayer) {
|
||||
Wallet.prototype.addCopayer = function(copayer) {
|
||||
this.copayers.push(copayer);
|
||||
|
||||
if (this.copayers.length < this.n) return;
|
||||
|
||||
|
||||
this.status = 'complete';
|
||||
this.publicKeyRing = _.pluck(this.copayers, 'xPubKey');
|
||||
};
|
||||
|
||||
Wallet.prototype.getCopayer = function (copayerId) {
|
||||
return _.find(this.copayers, { id: copayerId });
|
||||
Wallet.prototype.getCopayer = function(copayerId) {
|
||||
return _.find(this.copayers, {
|
||||
id: copayerId
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Wallet.prototype._getBitcoreNetwork = function () {
|
||||
Wallet.prototype._getBitcoreNetwork = function() {
|
||||
return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet;
|
||||
};
|
||||
|
||||
|
||||
Wallet.prototype.createAddress = function (path) {
|
||||
Wallet.prototype.createAddress = function(path) {
|
||||
|
||||
var publicKeys = _.map(this.copayers, function(copayer) {
|
||||
var xpub = new Bitcore.HDPublicKey(copayer.xPubKey);
|
||||
|
|
133
lib/server.js
133
lib/server.js
|
@ -31,12 +31,12 @@ var TxProposal = require('./model/txproposal');
|
|||
*/
|
||||
function CopayServer(opts) {
|
||||
opts = opts || {};
|
||||
this.storage = opts.storage || new Storage();
|
||||
this.storage = opts.storage || new Storage();
|
||||
};
|
||||
|
||||
inherits(CopayServer, events.EventEmitter);
|
||||
|
||||
CopayServer._emit = function (event) {
|
||||
CopayServer._emit = function(event) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
log.debug('Emitting: ', args);
|
||||
this.emit.apply(this, arguments);
|
||||
|
@ -52,8 +52,9 @@ CopayServer._emit = function (event) {
|
|||
* @param {string} opts.pubKey - Public key to verify copayers joining have access to the wallet secret.
|
||||
* @param {string} [opts.network = 'livenet'] - The Bitcoin network for this wallet.
|
||||
*/
|
||||
CopayServer.prototype.createWallet = function (opts, cb) {
|
||||
var self = this, pubKey;
|
||||
CopayServer.prototype.createWallet = function(opts, cb) {
|
||||
var self = this,
|
||||
pubKey;
|
||||
|
||||
Utils.checkRequired(opts, ['id', 'name', 'm', 'n', 'pubKey']);
|
||||
if (!Wallet.verifyCopayerLimits(opts.m, opts.n)) return cb('Incorrect m or n value');
|
||||
|
@ -66,7 +67,7 @@ CopayServer.prototype.createWallet = function (opts, cb) {
|
|||
return cb(e.toString());
|
||||
};
|
||||
|
||||
self.storage.fetchWallet(opts.id, function (err, wallet) {
|
||||
self.storage.fetchWallet(opts.id, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
if (wallet) return cb('Wallet already exists');
|
||||
|
||||
|
@ -89,10 +90,10 @@ CopayServer.prototype.createWallet = function (opts, cb) {
|
|||
* @param {string} opts.id - The wallet id.
|
||||
* @returns {Object} wallet
|
||||
*/
|
||||
CopayServer.prototype.getWallet = function (opts, cb) {
|
||||
CopayServer.prototype.getWallet = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
self.storage.fetchWallet(opts.id, function (err, wallet) {
|
||||
self.storage.fetchWallet(opts.id, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
if (!wallet) return cb('Wallet not found');
|
||||
return cb(null, wallet);
|
||||
|
@ -119,13 +120,15 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) {
|
|||
* @param {number} opts.xPubKey - Extended Public Key for this copayer.
|
||||
* @param {number} opts.xPubKeySignature - Signature of xPubKey using the wallet pubKey.
|
||||
*/
|
||||
CopayServer.prototype.joinWallet = function (opts, cb) {
|
||||
CopayServer.prototype.joinWallet = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, ['walletId', 'id', 'name', 'xPubKey', 'xPubKeySignature']);
|
||||
|
||||
Utils.runLocked(opts.walletId, cb, function (cb) {
|
||||
self.getWallet({ id: opts.walletId }, function (err, wallet) {
|
||||
Utils.runLocked(opts.walletId, cb, function(cb) {
|
||||
self.getWallet({
|
||||
id: opts.walletId
|
||||
}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
if (!self._verifySignature(opts.xPubKey, opts.xPubKeySignature, wallet.pubKey)) {
|
||||
|
@ -144,10 +147,10 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) {
|
|||
xPubKeySignature: opts.xPubKeySignature,
|
||||
copayerIndex: wallet.copayers.length,
|
||||
});
|
||||
|
||||
|
||||
wallet.addCopayer(copayer);
|
||||
|
||||
self.storage.storeWallet(wallet, function (err) {
|
||||
self.storage.storeWallet(wallet, function(err) {
|
||||
return cb(err);
|
||||
});
|
||||
});
|
||||
|
@ -164,16 +167,18 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) {
|
|||
* @param {truthy} opts.isChange - Indicates whether this is a regular address or a change address.
|
||||
* @returns {Address} address
|
||||
*/
|
||||
CopayServer.prototype.createAddress = function (opts, cb) {
|
||||
CopayServer.prototype.createAddress = function(opts, cb) {
|
||||
var self = this;
|
||||
var isChange = opts.isChange || false;
|
||||
|
||||
Utils.checkRequired(opts, ['walletId', 'isChange']);
|
||||
|
||||
Utils.runLocked(opts.walletId, cb, function (cb) {
|
||||
self.getWallet({ id: opts.walletId }, function (err, wallet) {
|
||||
Utils.runLocked(opts.walletId, cb, function(cb) {
|
||||
self.getWallet({
|
||||
id: opts.walletId
|
||||
}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
|
||||
var copayer = wallet.copayers[0]; // TODO: Assign copayer from authentication.
|
||||
|
||||
var path = copayer.getNewAddressPath(isChange);
|
||||
|
@ -202,12 +207,14 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) {
|
|||
* @param {string} opts.signature - The signature of message to verify.
|
||||
* @returns {truthy} The result of the verification.
|
||||
*/
|
||||
CopayServer.prototype.verifyMessageSignature = function (opts, cb) {
|
||||
CopayServer.prototype.verifyMessageSignature = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, ['walletId', 'copayerId', 'message', 'signature']);
|
||||
|
||||
self.getWallet({ id: opts.walletId }, function (err, wallet) {
|
||||
self.getWallet({
|
||||
id: opts.walletId
|
||||
}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var copayer = wallet.getCopayer(opts.copayerId);
|
||||
|
@ -219,7 +226,7 @@ CopayServer.prototype.verifyMessageSignature = function (opts, cb) {
|
|||
};
|
||||
|
||||
|
||||
CopayServer.prototype._getBlockExplorer = function (provider, network) {
|
||||
CopayServer.prototype._getBlockExplorer = function(provider, network) {
|
||||
var url;
|
||||
|
||||
switch (provider) {
|
||||
|
@ -239,33 +246,37 @@ CopayServer.prototype._getBlockExplorer = function (provider, network) {
|
|||
}
|
||||
};
|
||||
|
||||
CopayServer.prototype._getUtxos = function (opts, cb) {
|
||||
CopayServer.prototype._getUtxos = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
// Get addresses for this wallet
|
||||
self.storage.fetchAddresses(opts.walletId, function (err, addresses) {
|
||||
self.storage.fetchAddresses(opts.walletId, function(err, addresses) {
|
||||
if (err) return cb(err);
|
||||
if (addresses.length == 0) return cb('The wallet has no addresses');
|
||||
|
||||
var addresses = _.pluck(addresses, 'address');
|
||||
|
||||
var bc = self._getBlockExplorer('insight', opts.network);
|
||||
bc.getUnspentUtxos(addresses, function (err, utxos) {
|
||||
bc.getUnspentUtxos(addresses, function(err, utxos) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self.getPendingTxs({ walletId: opts.walletId }, function (err, txps) {
|
||||
self.getPendingTxs({
|
||||
walletId: opts.walletId
|
||||
}, function(err, txps) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var inputs = _.chain(txps)
|
||||
.pluck('inputs')
|
||||
.flatten()
|
||||
.map(function (utxo) { return utxo.txid + '|' + utxo.vout });
|
||||
.map(function(utxo) {
|
||||
return utxo.txid + '|' + utxo.vout
|
||||
});
|
||||
|
||||
var dictionary = _.groupBy(utxos, function (utxo) {
|
||||
var dictionary = _.groupBy(utxos, function(utxo) {
|
||||
return utxo.txid + '|' + utxo.vout;
|
||||
});
|
||||
|
||||
_.each(inputs, function (input) {
|
||||
_.each(inputs, function(input) {
|
||||
if (dictionary[input]) {
|
||||
dictionary[input].locked = true;
|
||||
}
|
||||
|
@ -283,17 +294,25 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
|
|||
* @param {string} opts.walletId - The wallet id.
|
||||
* @returns {Object} balance - Total amount & locked amount.
|
||||
*/
|
||||
CopayServer.prototype.getBalance = function (opts, cb) {
|
||||
CopayServer.prototype.getBalance = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, 'walletId');
|
||||
|
||||
self._getUtxos({ walletId: opts.walletId }, function (err, utxos) {
|
||||
self._getUtxos({
|
||||
walletId: opts.walletId
|
||||
}, function(err, utxos) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var balance = {};
|
||||
balance.totalAmount = _.reduce(utxos, function(sum, utxo) { return sum + utxo.amount; }, 0);
|
||||
balance.lockedAmount = _.reduce(_.without(utxos, { locked: true }), function(sum, utxo) { return sum + utxo.amount; }, 0);
|
||||
balance.totalAmount = _.reduce(utxos, function(sum, utxo) {
|
||||
return sum + utxo.amount;
|
||||
}, 0);
|
||||
balance.lockedAmount = _.reduce(_.without(utxos, {
|
||||
locked: true
|
||||
}), function(sum, utxo) {
|
||||
return sum + utxo.amount;
|
||||
}, 0);
|
||||
|
||||
return cb(null, balance);
|
||||
});
|
||||
|
@ -336,18 +355,24 @@ CopayServer.prototype._selectUtxos = function(txp, utxos) {
|
|||
* @param {string} opts.message - A message to attach to this transaction.
|
||||
* @returns {TxProposal} Transaction proposal.
|
||||
*/
|
||||
CopayServer.prototype.createTx = function (opts, cb) {
|
||||
CopayServer.prototype.createTx = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, ['walletId', 'copayerId', 'toAddress', 'amount', 'message']);
|
||||
|
||||
self.getWallet({ id: opts.walletId }, function (err, wallet) {
|
||||
self.getWallet({
|
||||
id: opts.walletId
|
||||
}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self._getUtxos({ walletId: wallet.id }, function (err, utxos) {
|
||||
self._getUtxos({
|
||||
walletId: wallet.id
|
||||
}, function(err, utxos) {
|
||||
if (err) return cb(err);
|
||||
|
||||
utxos = _.without(utxos, { locked: true });
|
||||
utxos = _.without(utxos, {
|
||||
locked: true
|
||||
});
|
||||
|
||||
var txp = new TxProposal({
|
||||
creatorId: opts.copayerId,
|
||||
|
@ -361,18 +386,18 @@ CopayServer.prototype.createTx = function (opts, cb) {
|
|||
|
||||
txp.rawTx = self._createRawTx(txp);
|
||||
|
||||
self.storage.storeTx(wallet.id, txp, function (err) {
|
||||
self.storage.storeTx(wallet.id, txp, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
return cb(null, txp);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
CopayServer.prototype._broadcastTx = function (rawTx, cb) {
|
||||
CopayServer.prototype._broadcastTx = function(rawTx, cb) {
|
||||
// TODO: this should attempt to broadcast _all_ accepted and not-yet broadcasted (status=='accepted') txps?
|
||||
cb = cb || function () {};
|
||||
cb = cb || function() {};
|
||||
|
||||
throw 'not implemented';
|
||||
};
|
||||
|
@ -385,29 +410,31 @@ CopayServer.prototype._broadcastTx = function (rawTx, cb) {
|
|||
* @param {string} opts.txProposalId - The identifier of the transaction.
|
||||
* @param {string} opts.signature - The signature of the tx for this copayer.
|
||||
*/
|
||||
CopayServer.prototype.signTx = function (opts, cb) {
|
||||
CopayServer.prototype.signTx = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signature']);
|
||||
|
||||
self.fetchTx(opts.walletId, opts.txProposalId, function (err, txp) {
|
||||
self.fetchTx(opts.walletId, opts.txProposalId, function(err, txp) {
|
||||
if (err) return cb(err);
|
||||
if (!txp) return cb('Transaction proposal not found');
|
||||
var action = _.find(txp.actions, { copayerId: opts.copayerId });
|
||||
var action = _.find(txp.actions, {
|
||||
copayerId: opts.copayerId
|
||||
});
|
||||
if (action) return cb('Copayer already voted on this transaction proposal');
|
||||
if (txp.status != 'pending') return cb('The transaction proposal is not pending');
|
||||
|
||||
txp.sign(opts.copayerId, opts.signature);
|
||||
|
||||
self.storage.storeTx(opts.walletId, txp, function (err) {
|
||||
self.storage.storeTx(opts.walletId, txp, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
if (txp.status == 'accepted');
|
||||
self._broadcastTx(txp.rawTx, function (err, txid) {
|
||||
self._broadcastTx(txp.rawTx, function(err, txid) {
|
||||
if (err) return cb(err);
|
||||
|
||||
tx.setBroadcasted(txid);
|
||||
self.storage.storeTx(opts.walletId, txp, function (err) {
|
||||
self.storage.storeTx(opts.walletId, txp, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
return cb();
|
||||
|
@ -425,21 +452,23 @@ CopayServer.prototype.signTx = function (opts, cb) {
|
|||
* @param {string} opts.txProposalId - The identifier of the transaction.
|
||||
* @param {string} [opts.reason] - A message to other copayers explaining the rejection.
|
||||
*/
|
||||
CopayServer.prototype.rejectTx = function (opts, cb) {
|
||||
CopayServer.prototype.rejectTx = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId']);
|
||||
|
||||
self.fetchTx(opts.walletId, opts.txProposalId, function (err, txp) {
|
||||
self.fetchTx(opts.walletId, opts.txProposalId, function(err, txp) {
|
||||
if (err) return cb(err);
|
||||
if (!txp) return cb('Transaction proposal not found');
|
||||
var action = _.find(txp.actions, { copayerId: opts.copayerId });
|
||||
var action = _.find(txp.actions, {
|
||||
copayerId: opts.copayerId
|
||||
});
|
||||
if (action) return cb('Copayer already voted on this transaction proposal');
|
||||
if (txp.status != 'pending') return cb('The transaction proposal is not pending');
|
||||
|
||||
txp.reject(opts.copayerId);
|
||||
|
||||
self.storage.storeTx(opts.walletId, txp, function (err) {
|
||||
self.storage.storeTx(opts.walletId, txp, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
return cb();
|
||||
|
@ -453,15 +482,17 @@ CopayServer.prototype.rejectTx = function (opts, cb) {
|
|||
* @param {string} opts.walletId - The wallet id.
|
||||
* @returns {TxProposal[]} Transaction proposal.
|
||||
*/
|
||||
CopayServer.prototype.getPendingTxs = function (opts, cb) {
|
||||
CopayServer.prototype.getPendingTxs = function(opts, cb) {
|
||||
var self = this;
|
||||
|
||||
Utils.checkRequired(opts, 'walletId');
|
||||
|
||||
self.storage.fetchTxs(opts.walletId, function (err, txps) {
|
||||
self.storage.fetchTxs(opts.walletId, function(err, txps) {
|
||||
if (err) return cb(err);
|
||||
|
||||
var pending = _.filter(txps, { status: 'pending' });
|
||||
var pending = _.filter(txps, {
|
||||
status: 'pending'
|
||||
});
|
||||
return cb(null, pending);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -53,7 +53,7 @@ var someXPubKeysSignatures = [
|
|||
|
||||
//Copayer signature
|
||||
var aText = 'hello world';
|
||||
var aTextSignature = '3045022100addd20e5413865d65d561ad2979f2289a40d52594b1f804840babd9a63e4ebbf02204b86285e1fcab02df772e7a1325fc4b511ecad79a8f80a2bd1ad8bfa858ac3d4'; // with someXPrivKey[0].derive('m/1/0')=5c0e043a513032907d181325a8e7990b076c0af15ed13dc5e611cda9bb3ae52a;
|
||||
var aTextSignature = '3045022100addd20e5413865d65d561ad2979f2289a40d52594b1f804840babd9a63e4ebbf02204b86285e1fcab02df772e7a1325fc4b511ecad79a8f80a2bd1ad8bfa858ac3d4'; // with someXPrivKey[0].derive('m/1/0')=5c0e043a513032907d181325a8e7990b076c0af15ed13dc5e611cda9bb3ae52a;
|
||||
|
||||
|
||||
var helpers = {};
|
||||
|
@ -432,7 +432,7 @@ describe('Copay server', function() {
|
|||
xPubKey: someXPubKeys[0],
|
||||
};
|
||||
try {
|
||||
server.joinWallet(copayerOpts, function(err) {});
|
||||
server.joinWallet(copayerOpts, function(err) {});
|
||||
} catch (e) {
|
||||
e.should.contain('xPubKeySignature');
|
||||
done();
|
||||
|
@ -477,7 +477,7 @@ describe('Copay server', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('should set pkr and status = complete on last copayer joining (2-3)', function(done) {
|
||||
helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) {
|
||||
|
@ -487,7 +487,7 @@ describe('Copay server', function() {
|
|||
should.not.exist(err);
|
||||
wallet.status.should.equal('complete');
|
||||
wallet.publicKeyRing.length.should.equal(3);
|
||||
_.each([0,1,2], function(i) {
|
||||
_.each([0, 1, 2], function(i) {
|
||||
var copayer = wallet.copayers[i];
|
||||
copayer.receiveAddressIndex.should.equal(0);
|
||||
copayer.changeAddressIndex.should.equal(0);
|
||||
|
@ -584,20 +584,17 @@ describe('Copay server', function() {
|
|||
server = new CopayServer({
|
||||
storage: storage,
|
||||
});
|
||||
server._doCreateAddress = sinon.stub().returns(new Address({
|
||||
address: 'addr1',
|
||||
path: 'path1'
|
||||
}));
|
||||
helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) {
|
||||
server.createAddress({
|
||||
walletId: '123'
|
||||
walletId: '123',
|
||||
isChange: false,
|
||||
}, function(err, address) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should create tx', function(done) {
|
||||
it.only('should create tx', function(done) {
|
||||
var bc = sinon.stub();
|
||||
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, helpers.createUtxos([100, 200]));
|
||||
server._getBlockExplorer = sinon.stub().returns(bc);
|
||||
|
|
Loading…
Reference in New Issue