@ -1230,7 +1230,11 @@ WalletService.prototype.getFeeLevels = function(opts, cb) {
} ) ;
} ;
WalletService . prototype . _ checkTxAndEstimateFee = function ( txp ) {
WalletService . prototype . _ estimateFee = function ( txp ) {
txp . estimateFee ( ) ;
} ;
WalletService . prototype . _ checkTx = function ( txp ) {
var bitcoreError ;
var serializationOpts = {
@ -1241,7 +1245,8 @@ WalletService.prototype._checkTxAndEstimateFee = function(txp) {
serializationOpts . disableLargeFees = true ;
}
txp . estimateFee ( ) ;
if ( txp . getEstimatedSize ( ) / 1000 > Defaults . MAX_TX_SIZE_IN_KB )
return Errors . TX_MAX_SIZE_EXCEEDED ;
try {
var bitcoreTx = txp . getBitcoreTx ( ) ;
@ -1264,40 +1269,164 @@ WalletService.prototype._checkTxAndEstimateFee = function(txp) {
WalletService . prototype . _ selectTxInputs = function ( txp , utxosToExclude , cb ) {
var self = this ;
//todo: check inputs are ours and has enough value
if ( txp . inputs && txp . inputs . length > 0 ) {
return cb ( self . _ checkTxAndEstimateFee ( txp ) ) ;
}
function sortUtxos ( utxos ) {
var list = _ . map ( utxos , function ( utxo ) {
var order ;
if ( utxo . confirmations == 0 ) {
order = 0 ;
} else if ( utxo . confirmations < 6 ) {
order = - 1 ;
} else {
order = - 2 ;
}
return {
order : order ,
utxo : utxo
} ;
} ) ;
return _ . pluck ( _ . sortBy ( list , 'order' ) , 'utxo' ) ;
} ;
//todo: check inputs are ours and have enough value
if ( txp . inputs && ! _ . isEmpty ( txp . inputs ) ) {
if ( ! _ . isNumber ( txp . fee ) )
self . _ estimateFee ( txp ) ;
return cb ( self . _ checkTx ( txp ) ) ;
}
self . _ getUtxosForCurrentWallet ( null , function ( err , utxos ) {
if ( err ) return cb ( err ) ;
var txpAmount = txp . getTotalAmount ( ) ;
var baseTxpSize = txp . getEstimatedSize ( ) ;
var baseTxpFee = baseTxpSize * txp . feePerKb / 1000. ;
var sizePerInput = txp . getEstimatedSizeForSingleInput ( ) ;
var feePerInput = sizePerInput * txp . feePerKb / 1000. ;
function sanitizeUtxos ( utxos ) {
var excludeIndex = _ . reduce ( utxosToExclude , function ( res , val ) {
res [ val ] = val ;
return res ;
} , { } ) ;
utxos = _ . reject ( utxos , function ( utxo ) {
return excludeIndex [ utxo . txid + ":" + utxo . vout ] ;
return _ . filter ( utxos , function ( utxo ) {
if ( utxo . locked ) return false ;
if ( utxo . satoshis <= feePerInput ) return false ;
if ( txp . excludeUnconfirmedUtxos && ! utxo . confirmations ) return false ;
if ( excludeIndex [ utxo . txid + ":" + utxo . vout ] ) return false ;
return true ;
} ) ;
} ;
function partitionUtxos ( utxos ) {
return _ . groupBy ( utxos , function ( utxo ) {
if ( utxo . confirmations == 0 ) return '0'
if ( utxo . confirmations < 6 ) return '<6' ;
return '6+' ;
} ) ;
} ;
function select ( utxos , cb ) {
var totalValueInUtxos = _ . sum ( utxos , 'satoshis' ) ;
var netValueInUtxos = totalValueInUtxos - baseTxpFee - ( utxos . length * feePerInput ) ;
if ( totalValueInUtxos < txpAmount ) {
log . debug ( 'Total value in all utxos (' + Utils . formatAmountInBtc ( totalValueInUtxos ) + ') is insufficient to cover for txp amount (' + Utils . formatAmountInBtc ( txpAmount ) + ')' ) ;
return cb ( Errors . INSUFFICIENT_FUNDS ) ;
}
if ( netValueInUtxos < txpAmount ) {
log . debug ( 'Value after fees in all utxos (' + Utils . formatAmountInBtc ( netValueInUtxos ) + ') is insufficient to cover for txp amount (' + Utils . formatAmountInBtc ( txpAmount ) + ')' ) ;
return cb ( Errors . INSUFFICIENT_FUNDS_FOR_FEE ) ;
}
var bigInputThreshold = txpAmount * Defaults . UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + ( baseTxpFee + feePerInput ) ;
log . debug ( 'Big input threshold ' + Utils . formatAmountInBtc ( bigInputThreshold ) ) ;
var partitions = _ . partition ( utxos , function ( utxo ) {
return utxo . satoshis > bigInputThreshold ;
} ) ;
var bigInputs = _ . sortBy ( partitions [ 0 ] , 'satoshis' ) ;
var smallInputs = _ . sortBy ( partitions [ 1 ] , function ( utxo ) {
return - utxo . satoshis ;
} ) ;
log . debug ( 'Considering ' + bigInputs . length + ' big inputs (' + Utils . formatUtxos ( bigInputs ) + ')' ) ;
log . debug ( 'Considering ' + smallInputs . length + ' small inputs (' + Utils . formatUtxos ( smallInputs ) + ')' ) ;
var total = 0 ;
var netTotal = - baseTxpFee ;
var selected = [ ] ;
var fee ;
var error ;
_ . each ( smallInputs , function ( input , i ) {
log . debug ( 'Input #' + i + ': ' + Utils . formatUtxos ( input ) ) ;
var netInputAmount = input . satoshis - feePerInput ;
log . debug ( 'The input contributes ' + Utils . formatAmountInBtc ( netInputAmount ) ) ;
selected . push ( input ) ;
total += input . satoshis ;
netTotal += netInputAmount ;
var txpSize = baseTxpSize + selected . length * sizePerInput ;
fee = Math . round ( baseTxpFee + selected . length * feePerInput ) ;
log . debug ( 'Tx size: ' + Utils . formatSize ( txpSize ) + ', Tx fee: ' + Utils . formatAmountInBtc ( fee ) ) ;
var feeVsAmountRatio = fee / txpAmount ;
var amountVsUtxoRatio = netInputAmount / txpAmount ;
log . debug ( 'Fee/Tx amount: ' + Utils . formatRatio ( feeVsAmountRatio ) + ' (max: ' + Utils . formatRatio ( Defaults . UTXO_SELECTION_MAX_FEE_VS_TX_AMOUNT_FACTOR ) + ')' ) ;
log . debug ( 'Tx amount/Input amount:' + Utils . formatRatio ( amountVsUtxoRatio ) + ' (min: ' + Utils . formatRatio ( Defaults . UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR ) + ')' ) ;
if ( txpSize / 1000. > Defaults . MAX_TX_SIZE_IN_KB ) {
log . debug ( 'Breaking because tx size (' + Utils . formatSize ( txpSize ) + ') is too big (max: ' + Utils . formatSize ( Defaults . MAX_TX_SIZE_IN_KB * 1000. ) + ')' ) ;
error = Errors . TX_MAX_SIZE_EXCEEDED ;
return false ;
}
if ( ! _ . isEmpty ( bigInputs ) ) {
if ( amountVsUtxoRatio < Defaults . UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR ) {
log . debug ( 'Breaking because utxo is too small compared to tx amount' ) ;
return false ;
}
if ( feeVsAmountRatio > Defaults . UTXO_SELECTION_MAX_FEE_VS_TX_AMOUNT_FACTOR ) {
var feeVsSingleInputFeeRatio = fee / ( baseTxpFee + feePerInput ) ;
log . debug ( 'Fee/Single-input fee: ' + Utils . formatRatio ( feeVsSingleInputFeeRatio ) + ' (max: ' + Utils . formatRatio ( Defaults . UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR ) + ')' + ' loses wrt single-input tx: ' + Utils . formatAmountInBtc ( ( selected . length - 1 ) * feePerInput ) ) ;
if ( feeVsSingleInputFeeRatio > Defaults . UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR ) {
log . debug ( 'Breaking because fee is too significant compared to tx amount and it is too expensive compared to using single input' ) ;
return false ;
}
}
}
log . debug ( 'Cumuled total so far: ' + Utils . formatAmountInBtc ( total ) + ', Net total so far: ' + Utils . formatAmountInBtc ( netTotal ) ) ;
if ( netTotal >= txpAmount ) {
var changeAmount = Math . round ( total - txpAmount - fee ) ;
log . debug ( 'Tx change: ' , Utils . formatAmountInBtc ( changeAmount ) ) ;
if ( changeAmount > 0 && changeAmount <= Bitcore . Transaction . DUST_AMOUNT ) {
log . debug ( 'Change below dust amount (' + Utils . formatAmountInBtc ( Bitcore . Transaction . DUST_AMOUNT ) + ')' ) ;
// Remove dust change by incrementing fee
fee += changeAmount ;
}
return false ;
}
} ) ;
if ( netTotal < txpAmount ) {
log . debug ( 'Could not reach Txp total (' + Utils . formatAmountInBtc ( txpAmount ) + '), still missing: ' + Utils . formatAmountInBtc ( txpAmount - netTotal ) ) ;
selected = [ ] ;
if ( ! _ . isEmpty ( bigInputs ) ) {
var input = _ . first ( bigInputs ) ;
log . debug ( 'Using big input: ' , Utils . formatUtxos ( input ) ) ;
total = input . satoshis ;
fee = Math . round ( baseTxpFee + feePerInput ) ;
netTotal = total - fee ;
selected = [ input ] ;
}
}
if ( _ . isEmpty ( selected ) ) {
log . debug ( 'Could not find enough funds within this utxo subset' ) ;
return cb ( error || Errors . INSUFFICIENT_FUNDS_FOR_FEE ) ;
}
return cb ( null , selected , fee ) ;
} ;
log . debug ( 'Selecting inputs for a ' + Utils . formatAmountInBtc ( txp . getTotalAmount ( ) ) + ' txp' ) ;
self . _ getUtxosForCurrentWallet ( null , function ( err , utxos ) {
if ( err ) return cb ( err ) ;
var totalAmount ;
var availableAmount ;
@ -1314,36 +1443,72 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) {
if ( totalAmount < txp . getTotalAmount ( ) ) return cb ( Errors . INSUFFICIENT_FUNDS ) ;
if ( availableAmount < txp . getTotalAmount ( ) ) return cb ( Errors . LOCKED_FUNDS ) ;
// Prepare UTXOs list
utxos = _ . reject ( utxos , 'locked' ) ;
if ( txp . excludeUnconfirmedUtxos ) {
utxos = _ . filter ( utxos , 'confirmations' ) ;
}
utxos = sanitizeUtxos ( utxos ) ;
log . debug ( 'Considering ' + utxos . length + ' utxos (' + Utils . formatUtxos ( utxos ) + ')' ) ;
var groups = [ 6 , 1 ] ;
if ( ! txp . excludeUnconfirmedUtxos ) groups . push ( 0 ) ;
var inputs = [ ] ;
var fee ;
var selectionError ;
var i = 0 ;
var total = 0 ;
var selected = [ ] ;
var inputs = sortUtxos ( utxos ) ;
var lastGroupLength ;
async . whilst ( function ( ) {
return i < groups . length && _ . isEmpty ( inputs ) ;
} , function ( next ) {
var group = groups [ i ++ ] ;
var bitcoreTx , bitcoreError ;
var candidateUtxos = _ . filter ( utxos , function ( utxo ) {
return utxo . confirmations >= group ;
} ) ;
function select ( ) {
if ( i >= inputs . length ) return cb ( bitcoreError || new Error ( 'Could not select tx inputs' ) ) ;
log . debug ( 'Group >= ' + group ) ;
var input = inputs [ i ++ ] ;
selected . push ( input ) ;
total += input . satoshis ;
if ( total >= txp . getTotalAmount ( ) ) {
txp . setInputs ( selected ) ;
bitcoreError = self . _ checkTxAndEstimateFee ( txp ) ;
if ( ! bitcoreError ) return cb ( ) ;
if ( txp . getEstimatedSize ( ) / 1000 > Defaults . MAX_TX_SIZE_IN_KB )
return cb ( Errors . TX_MAX_SIZE_EXCEEDED ) ;
// If this group does not have any new elements, skip it
if ( lastGroupLength === candidateUtxos . length ) {
log . debug ( 'This group is identical to the one already explored' ) ;
return next ( ) ;
}
setTimeout ( select , 0 ) ;
} ;
select ( ) ;
log . debug ( 'Candidate utxos: ' + Utils . formatUtxos ( candidateUtxos ) ) ;
lastGroupLength = candidateUtxos . length ;
select ( candidateUtxos , function ( err , selectedInputs , selectedFee ) {
if ( err ) {
log . debug ( 'No inputs selected on this group: ' , err ) ;
selectionError = err ;
return next ( ) ;
}
selectionError = null ;
inputs = selectedInputs ;
fee = selectedFee ;
log . debug ( 'Selected inputs from this group: ' + Utils . formatUtxos ( inputs ) ) ;
log . debug ( 'Fee for this selection: ' + Utils . formatAmountInBtc ( fee ) ) ;
return next ( ) ;
} ) ;
} , function ( err ) {
if ( err ) return cb ( err ) ;
if ( selectionError || _ . isEmpty ( inputs ) ) return cb ( selectionError || new Error ( 'Could not select tx inputs' ) ) ;
txp . setInputs ( _ . shuffle ( inputs ) ) ;
txp . fee = fee ;
var err = self . _ checkTx ( txp ) ;
if ( ! err ) {
log . debug ( 'Successfully built transaction. Total fees: ' + Utils . formatAmountInBtc ( txp . fee ) + ', total change: ' + Utils . formatAmountInBtc ( _ . sum ( txp . inputs , 'satoshis' ) - txp . fee ) ) ;
} else {
log . warn ( 'Error building transaction' , err ) ;
}
return cb ( err ) ;
} ) ;
} ) ;
} ;
@ -1559,21 +1724,28 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
* @ param { number } opts . outputs [ ] . amount - Amount to transfer in satoshi .
* @ param { string } opts . outputs [ ] . message - A message to attach to this output .
* @ param { string } opts . message - A message to attach to this transaction .
* @ param { Array } opts . inputs - Optional . Inputs for this TX
* @ param { string } opts . feePerKb - The fee per kB to use for this TX .
* @ param { number } opts . feePerKb - The fee per kB to use for this TX .
* @ param { string } opts . payProUrl - Optional . Paypro URL for peers to verify TX
* @ param { string } opts . excludeUnconfirmedUtxos [ = false ] - Optional . Do not use UTXOs of unconfirmed transactions as inputs
* @ param { string } opts . validateOutputs [ = true ] - Optional . Perform validation on outputs .
* @ param { Array } opts . inputs - Optional . Inputs for this TX
* @ param { number } opts . fee - Optional . The fee to use for this TX ( used only when opts . inputs is specified ) .
* @ returns { TxProposal } Transaction proposal .
* /
WalletService . prototype . createTx = function ( opts , cb ) {
var self = this ;
if ( ! Utils . checkRequired ( opts , [ 'outputs' , 'feePerKb' ] ) )
if ( ! Utils . checkRequired ( opts , [ 'outputs' ] ) )
return cb ( new ClientError ( 'Required argument missing' ) ) ;
// feePerKb is required unless inputs & fee are specified
if ( ! _ . isNumber ( opts . feePerKb ) && ! ( opts . inputs && _ . isNumber ( opts . fee ) ) )
return cb ( new ClientError ( 'Required argument missing' ) ) ;
if ( opts . feePerKb < Defaults . MIN_FEE_PER_KB || opts . feePerKb > Defaults . MAX_FEE_PER_KB )
return cb ( new ClientError ( 'Invalid fee per KB' ) ) ;
if ( _ . isNumber ( opts . feePerKb ) ) {
if ( opts . feePerKb < Defaults . MIN_FEE_PER_KB || opts . feePerKb > Defaults . MAX_FEE_PER_KB )
return cb ( new ClientError ( 'Invalid fee per KB' ) ) ;
}
self . _ runLocked ( cb , function ( cb ) {
self . getWallet ( { } , function ( err , wallet ) {
@ -1594,7 +1766,6 @@ WalletService.prototype.createTx = function(opts, cb) {
var txOpts = {
walletId : self . walletId ,
creatorId : self . copayerId ,
inputs : opts . inputs ,
outputs : opts . outputs ,
message : opts . message ,
changeAddress : wallet . createAddress ( true ) ,
@ -1606,6 +1777,8 @@ WalletService.prototype.createTx = function(opts, cb) {
validateOutputs : ! opts . validateOutputs ,
addressType : wallet . addressType ,
customData : opts . customData ,
inputs : opts . inputs ,
fee : opts . inputs && ! _ . isNumber ( opts . feePerKb ) ? opts . fee : null ,
} ;
var txp = Model . TxProposal . create ( txOpts ) ;