|
|
@ -49,6 +49,19 @@ function WalletService() { |
|
|
|
this.notifyTicker = 0; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Time after which a Tx proposal can be erased by any copayer. in seconds
|
|
|
|
WalletService.deleteLockTime = 24 * 3600; |
|
|
|
|
|
|
|
// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in seconds.
|
|
|
|
WalletService.backoffTime = 2 * 60; |
|
|
|
|
|
|
|
// Fund scanning parameters
|
|
|
|
WalletService.scanConfig = { |
|
|
|
SCAN_WINDOW: 20, |
|
|
|
DERIVATION_DELAY: 10, // in milliseconds
|
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Initializes global settings for all instances. |
|
|
|
* @param {Object} opts |
|
|
@ -731,6 +744,33 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
WalletService.prototype._canCreateTx = function(copayerId, cb) { |
|
|
|
var self = this; |
|
|
|
self.storage.fetchLastTxs(self.walletId, copayerId, 5, function(err, txs) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!txs.length) |
|
|
|
return cb(null, true); |
|
|
|
|
|
|
|
var lastRejections = _.takeWhile(txs, {status: 'rejected'}); |
|
|
|
|
|
|
|
if (!lastRejections.length) |
|
|
|
return cb(null, true); |
|
|
|
|
|
|
|
var lastTxTs = txs[0].createdOn; |
|
|
|
var now = Math.floor(Date.now() / 1000); |
|
|
|
var timeSinceLastRejection = now - lastTxTs; |
|
|
|
var backoffTime = WalletService.backoffTime * lastRejections.length; |
|
|
|
|
|
|
|
if (timeSinceLastRejection <= backoffTime) |
|
|
|
log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', timeSinceLastRejection, backoffTime); |
|
|
|
|
|
|
|
return cb(null, timeSinceLastRejection > backoffTime); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Creates a new transaction proposal. |
|
|
|
* @param {Object} opts |
|
|
@ -750,59 +790,66 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
self._runLocked(cb, function(cb) { |
|
|
|
self.getWallet({}, function(err, wallet) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!wallet.isComplete()) return cb(new ClientError('Wallet is not complete')); |
|
|
|
|
|
|
|
var copayer = wallet.getCopayer(self.copayerId); |
|
|
|
var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); |
|
|
|
if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) |
|
|
|
return cb(new ClientError('Invalid proposal signature')); |
|
|
|
|
|
|
|
var toAddress; |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(opts.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
return cb(new ClientError('INVALIDADDRESS', 'Invalid address')); |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) |
|
|
|
return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); |
|
|
|
if (!wallet.isComplete()) |
|
|
|
return cb(new ClientError('Wallet is not complete')); |
|
|
|
|
|
|
|
if (opts.amount <= 0) |
|
|
|
return cb(new ClientError('Invalid amount')); |
|
|
|
self._canCreateTx(self.copayerId, function(err, canCreate) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!canCreate) |
|
|
|
return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time')); |
|
|
|
|
|
|
|
var copayer = wallet.getCopayer(self.copayerId); |
|
|
|
var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); |
|
|
|
if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) |
|
|
|
return cb(new ClientError('Invalid proposal signature')); |
|
|
|
|
|
|
|
var toAddress; |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(opts.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
return cb(new ClientError('INVALIDADDRESS', 'Invalid address')); |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) |
|
|
|
return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); |
|
|
|
|
|
|
|
if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) |
|
|
|
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); |
|
|
|
if (opts.amount <= 0) |
|
|
|
return cb(new ClientError('Invalid amount')); |
|
|
|
|
|
|
|
if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) |
|
|
|
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); |
|
|
|
|
|
|
|
var changeAddress = wallet.createAddress(true); |
|
|
|
|
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
toAddress: opts.toAddress, |
|
|
|
amount: opts.amount, |
|
|
|
message: opts.message, |
|
|
|
proposalSignature: opts.proposalSignature, |
|
|
|
payProUrl: opts.payProUrl, |
|
|
|
changeAddress: changeAddress, |
|
|
|
requiredSignatures: wallet.m, |
|
|
|
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), |
|
|
|
}); |
|
|
|
var changeAddress = wallet.createAddress(true); |
|
|
|
|
|
|
|
self._selectTxInputs(txp, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
$.checkState(txp.inputs); |
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
toAddress: opts.toAddress, |
|
|
|
amount: opts.amount, |
|
|
|
message: opts.message, |
|
|
|
proposalSignature: opts.proposalSignature, |
|
|
|
payProUrl: opts.payProUrl, |
|
|
|
changeAddress: changeAddress, |
|
|
|
requiredSignatures: wallet.m, |
|
|
|
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), |
|
|
|
}); |
|
|
|
|
|
|
|
self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) { |
|
|
|
self._selectTxInputs(txp, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
self.storage.storeTx(wallet.id, txp, function(err) { |
|
|
|
$.checkState(txp.inputs); |
|
|
|
|
|
|
|
self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
self._notify('NewTxProposal', { |
|
|
|
amount: opts.amount |
|
|
|
}, function() { |
|
|
|
return cb(null, txp); |
|
|
|
self.storage.storeTx(wallet.id, txp, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
self._notify('NewTxProposal', { |
|
|
|
amount: opts.amount |
|
|
|
}, function() { |
|
|
|
return cb(null, txp); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
@ -1339,13 +1386,6 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// in seconds
|
|
|
|
WalletService.deleteLockTime = 24 * 3600; |
|
|
|
|
|
|
|
WalletService.scanConfig = { |
|
|
|
SCAN_WINDOW: 20, |
|
|
|
DERIVATION_DELAY: 10, // in milliseconds
|
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Scan the blockchain looking for addresses having some activity |
|
|
|