coinselectsyscoin
Version:
A transaction input selection module for syscoin.
249 lines (218 loc) • 9.53 kB
JavaScript
const utils = require('./utils')
const ext = require('./bn-extensions')
const BN = require('bn.js')
// add inputs until we reach or surpass the target value (or deplete)
// worst-case: O(n)
function accumulative (utxos, inputs, outputs, feeRate, memoSize, blobSize, maxTxSize) {
// Default max transaction size (conservative limit for most networks)
maxTxSize = maxTxSize || 99000 // 99KB - leaving room for signatures and safety margin
if (!utils.uintOrNull(feeRate)) return { error: 'INVALID_FEE_RATE' }
const changeOutputBytes = utils.outputBytes({})
let memoPadding = 0
if (memoSize) {
memoPadding = memoSize + 5 + 8 // opreturn overhead + memo size + amount int64
}
blobSize = blobSize || 0
// Determine sweep strategy based on subtractFeeFrom distribution
const subtractFeeCount = outputs.filter(o => o.subtractFeeFrom === true).length
const totalOutputs = outputs.length
// Use all inputs if:
// 1. ALL outputs have subtractFeeFrom (true sweep), OR
// 2. MAJORITY of outputs have subtractFeeFrom (sweep-like behavior)
const shouldUseAllInputs = subtractFeeCount === totalOutputs ||
(subtractFeeCount > 0 && subtractFeeCount >= totalOutputs / 2)
const hasSomeSubtractFee = subtractFeeCount > 0
let feeBytes = new BN(changeOutputBytes.toNumber() + 4)
let bytesAccum = utils.transactionBytes(inputs, outputs)
let inAccum = utils.sumOrNaN(inputs)
let outAccum = utils.sumOrNaN(outputs)
// Check for invalid amounts early but continue to calculate proper fee
const hasInvalidAmounts = !inAccum || !outAccum
let fee = ext.mul(feeRate, bytesAccum)
const memBytes = new BN(memoPadding)
let blobBytes = new BN(blobSize)
// factor blobs by 100x in fee market
blobBytes = ext.mul(blobBytes, new BN(0.01))
bytesAccum = ext.add(bytesAccum, memBytes)
feeBytes = ext.add(feeBytes, memBytes)
feeBytes = ext.add(feeBytes, blobBytes)
const dustAmount = utils.dustThreshold({ type: 'BECH32' }, feeRate)
if (blobSize) {
outAccum = ext.add(outAccum, dustAmount)
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
// double up to be safe
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
}
// is already enough input?
if (!hasInvalidAmounts && !shouldUseAllInputs && ext.gte(inAccum, ext.add(outAccum, fee))) {
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
const utxoBytes = utils.inputBytes(utxo)
const utxoFee = ext.mul(feeRate, utxoBytes)
const utxoValue = utils.uintOrNull(utxo.value)
// skip detrimental input
if (ext.gt(utxoFee, utxoValue)) {
// Don't skip detrimental UTXOs in sweep mode (subtractFeeFrom)
// In sweep mode, fees are deducted from output value, so individual UTXO profitability doesn't matter
if (shouldUseAllInputs || hasSomeSubtractFee) {
// Add the UTXO even if it's detrimental in sweep mode
bytesAccum = ext.add(bytesAccum, utxoBytes)
// Check transaction size limit
if (bytesAccum.gt(new BN(maxTxSize))) {
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
inAccum = ext.add(inAccum, utxoValue)
inputs.push(utxo)
// Continue to next UTXO
continue
}
// Original logic: skip detrimental UTXOs in normal mode
if (i === utxos.length - 1) {
const calculatedFee = ext.mul(feeRate, ext.add(bytesAccum, utxoBytes))
const totalRequired = ext.add(outAccum, calculatedFee)
const shortfall = ext.sub(totalRequired, inAccum)
return {
error: 'INSUFFICIENT_FUNDS',
fee: calculatedFee,
shortfall,
details: {
inputTotal: inAccum,
outputTotal: outAccum,
requiredFee: calculatedFee,
message: 'Last UTXO costs more in fees than its value'
}
}
}
continue
}
bytesAccum = ext.add(bytesAccum, utxoBytes)
// Check if adding this input would exceed transaction size limit
if (bytesAccum.gt(new BN(maxTxSize))) {
// Don't add this input, use what we have so far
if ((shouldUseAllInputs || hasSomeSubtractFee) && inputs.length > 0) {
// For sweep or subtractFeeFrom, use current inputs even if we hit size limit
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
// For normal transactions, this means we can't fit enough inputs
break
}
inAccum = ext.add(inAccum, utxoValue)
inputs.push(utxo)
// if this is an asset input, we will need another output to send asset to so add dust satoshi to output and add output fee
if (utxo.assetInfo) {
outAccum = ext.add(outAccum, dustAmount)
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
// double up to be safe
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
// add another bech32 output for OP_RETURN overhead
// any extra data should be optimized out later as OP_RETURN is serialized and fees are optimized
bytesAccum = ext.add(bytesAccum, changeOutputBytes)
feeBytes = ext.add(feeBytes, changeOutputBytes)
}
fee = ext.mul(feeRate, bytesAccum)
// go again?
if (!shouldUseAllInputs && ext.lt(inAccum, ext.add(outAccum, fee))) {
continue
}
// For sweep operations, continue collecting ALL inputs (respecting size limits)
if (shouldUseAllInputs) {
continue
}
// Don't call finalize if we have invalid amounts
if (hasInvalidAmounts) {
break
}
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
// If sweep is specified and we've gone through all utxos,
// use all inputs collected
if (!hasInvalidAmounts && shouldUseAllInputs && inputs.length > 0) {
return utils.finalize(inputs, outputs, feeRate, feeBytes)
}
const calculatedFee = ext.mul(feeRate, bytesAccum)
// Check if we failed due to invalid amounts
if (!inAccum || !outAccum) {
return {
fee: calculatedFee,
error: 'INVALID_AMOUNT'
}
}
return {
fee: calculatedFee,
error: 'INSUFFICIENT_FUNDS',
shortfall: ext.sub(ext.add(outAccum, calculatedFee), inAccum),
details: {
inputTotal: inAccum,
outputTotal: outAccum,
requiredFee: calculatedFee,
message: 'Not enough UTXOs to cover amount and fees'
}
}
}
// worst-case: O(n)
function accumulativeAsset (utxoAssets, assetMap, feeRate, txVersion) {
if (!utils.uintOrNull(feeRate)) return { error: 'INVALID_FEE_RATE' }
const dustAmount = utils.dustThreshold({ type: 'BECH32' }, feeRate)
const isNonAssetFunded = utils.isNonAssetFunded(txVersion)
const assetAllocations = []
const outputs = []
const inputs = []
// loop through all assets looking to get funded, sort the utxo's and then try to fund them incrementally
for (const [assetGuid, valueAssetObj] of assetMap.entries()) {
const assetAllocation = { assetGuid, values: [] }
valueAssetObj.outputs.forEach(output => {
assetAllocation.values.push({ n: outputs.length, value: output.value })
if (output.address === valueAssetObj.changeAddress) {
// add change index
outputs.push({ assetChangeIndex: assetAllocation.values.length - 1, type: 'BECH32', assetInfo: { assetGuid, value: output.value }, value: dustAmount })
} else {
outputs.push({ address: output.address, type: 'BECH32', assetInfo: { assetGuid, value: output.value }, value: dustAmount })
}
})
// order by descending asset amounts for this asset guid
let utxoAsset = utxoAssets.filter(utxo => utxo.assetInfo.assetGuid === assetGuid)
utxoAsset = utxoAsset.concat().sort(function (a, b) {
return ext.sub(b.assetInfo.value, a.assetInfo.value)
})
if (!isNonAssetFunded) {
const assetOutAccum = utils.sumOrNaN(valueAssetObj.outputs)
// order by descending asset amounts for this asset guid
let utxoAsset = utxoAssets.filter(utxo => utxo.assetInfo.assetGuid === assetGuid)
utxoAsset = utxoAsset.concat().sort(function (a, b) {
return ext.sub(b.assetInfo.value, a.assetInfo.value)
})
let inAccum = ext.BN_ZERO
for (let i = 0; i < utxoAsset.length; i++) {
const utxo = utxoAsset[i]
const utxoValue = utils.uintOrNull(utxo.assetInfo.value)
inAccum = ext.add(inAccum, utxoValue)
inputs.push(utxo)
// deal with change
if (ext.gt(inAccum, assetOutAccum)) {
const changeAsset = ext.sub(inAccum, assetOutAccum)
// add output as dust amount (smallest possible sys output)
const output = { assetChangeIndex: assetAllocation.values.length, type: 'BECH32', assetInfo: { assetGuid, value: changeAsset }, value: dustAmount }
// but asset commitment will have the full asset change value
assetAllocation.values.push({ n: outputs.length, value: changeAsset })
outputs.push(output)
break
// no change, in = out
} else if (ext.eq(inAccum, assetOutAccum)) {
break
}
}
}
assetAllocations.push(assetAllocation)
}
return utils.finalizeAssets(inputs, outputs, assetAllocations)
}
module.exports = {
accumulative,
accumulativeAsset
}