UNPKG

coinselectsyscoin

Version:

A transaction input selection module for syscoin.

222 lines (194 loc) 6.24 kB
const BN = require('bn.js') const ext = require('./bn-extensions') const SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION = 139 const SYSCOIN_TX_VERSION_ALLOCATION_MINT = 140 function isNonAssetFunded (txVersion) { return txVersion === SYSCOIN_TX_VERSION_SYSCOIN_BURN_TO_ALLOCATION || txVersion === SYSCOIN_TX_VERSION_ALLOCATION_MINT } // baseline estimates, used to improve performance const TX_BASE_SIZE = new BN(11) const TX_INPUT_SIZE = { LEGACY: new BN(147), P2SH: new BN(91), BECH32: new BN(68) } const TX_OUTPUT_SIZE = { LEGACY: new BN(34), P2SH: new BN(32), BECH32: new BN(31) } function inputBytes (input) { return TX_INPUT_SIZE[input.type] || TX_INPUT_SIZE.LEGACY } function outputBytes (output) { if (output.script) { return new BN(output.script.length + 5 + 8) // 5 for OP_PUSHDATA2 max OP_RETURN prefix, 8 for amount } return TX_OUTPUT_SIZE[output.type] || TX_OUTPUT_SIZE.LEGACY } function dustThreshold (output, feeRate) { /* ... classify the output for input estimate */ return ext.mul(inputBytes(output), feeRate) } function transactionBytes (inputs, outputs) { return TX_BASE_SIZE .add(inputs.reduce(function (a, x) { return ext.add(a, inputBytes(x)) }, ext.BN_ZERO)) .add(outputs.reduce(function (a, x) { return ext.add(a, outputBytes(x)) }, ext.BN_ZERO)) } function uintOrNull (v) { if (!BN.isBN(v)) return null if (v.isNeg()) return null return v } function sumForgiving (range) { return range.reduce(function (a, x) { const valueOrZero = BN.isBN(x.value) ? x.value : ext.BN_ZERO return ext.add(a, valueOrZero) }, ext.BN_ZERO) } function sumOrNaN (range) { return range.reduce(function (a, x) { const value = x.value return ext.add(a, uintOrNull(value)) }, ext.BN_ZERO) } function finalize (inputs, outputs, feeRate, feeBytes, txVersion) { const bytesAccum = transactionBytes(inputs, outputs) const feeAfterExtraOutput = ext.mul(feeRate, ext.add(bytesAccum, feeBytes)) const inputTotal = sumOrNaN(inputs) const outputTotal = sumOrNaN(outputs, txVersion) const remainderAfterExtraOutput = ext.sub(inputTotal, ext.add(outputTotal, feeAfterExtraOutput)) // Fundamental validation: input must ALWAYS be >= output regardless of subtractFeeFrom // subtractFeeFrom can only reduce outputs, never create value from nothing if (inputTotal && outputTotal && inputTotal.lt(outputTotal)) { const shortfall = outputTotal.sub(inputTotal) return { error: 'INSUFFICIENT_FUNDS', fee: ext.BN_ZERO, shortfall, details: { inputTotal, outputTotal, requiredFee: ext.BN_ZERO, message: 'Input value is less than output value' } } } // Check if any outputs have subtractFeeFrom flag const subtractFeeOutputs = outputs.map((output, index) => ({ output, index })) .filter(item => item.output.subtractFeeFrom === true) if (subtractFeeOutputs.length > 0) { // Calculate fee without change output let fee = ext.mul(feeRate, bytesAccum) const outputsCopy = outputs.slice() let remainingFee = fee const outputsToRemove = [] // Subtract fees from marked outputs in order for (const { output, index } of subtractFeeOutputs) { const outputValue = output.value let deduction = ext.BN_ZERO const dust = dustThreshold(output, feeRate) if (!remainingFee.isZero()) { const maxDeduction = outputValue.sub(dust) if (!maxDeduction.isNeg() && !maxDeduction.isZero()) { deduction = remainingFee.lt(maxDeduction) ? remainingFee : maxDeduction remainingFee = remainingFee.sub(deduction) } } const newValue = outputValue.sub(deduction) // If the output value after deduction is at or below dust threshold, mark it for removal if (newValue.lte(dust)) { outputsToRemove.push(index) // If we're removing this output, the full value is effectively deducted remainingFee = remainingFee.sub(outputValue.sub(deduction)) } else { outputsCopy[index] = Object.assign({}, output, { value: newValue }) delete outputsCopy[index].subtractFeeFrom } } // Remove outputs marked for removal (in reverse order to maintain indices) for (let i = outputsToRemove.length - 1; i >= 0; i--) { outputsCopy.splice(outputsToRemove[i], 1) } // If we removed outputs, recalculate the fee with the new transaction size if (outputsToRemove.length > 0) { const newBytesAccum = transactionBytes(inputs, outputsCopy) fee = ext.mul(feeRate, newBytesAccum) } // If we couldn't subtract all fees, return error if (!remainingFee.isZero()) { return { error: 'SUBTRACT_FEE_FAILED', fee, remainingFee, details: { markedOutputs: subtractFeeOutputs.length, removedOutputs: outputsToRemove.length } } } return { inputs, outputs: outputsCopy, fee } } // Normal case: add change output if needed if (ext.gt(remainderAfterExtraOutput, dustThreshold({}, feeRate))) { outputs = outputs.concat({ changeIndex: outputs.length, value: remainderAfterExtraOutput }) } const fee = ext.sub(inputTotal, sumOrNaN(outputs, txVersion)) if (!fee) { const calculatedFee = ext.mul(feeRate, bytesAccum) const shortfall = ext.sub(ext.add(outputTotal, calculatedFee), inputTotal) return { error: 'INSUFFICIENT_FUNDS', fee: calculatedFee, shortfall, details: { inputTotal, outputTotal, requiredFee: calculatedFee, message: 'Insufficient funds after accounting for fees' } } } return { inputs, outputs, fee } } function finalizeAssets (inputs, outputs, assetAllocations) { if (!inputs || !outputs || !assetAllocations) { return { inputs: null, outputs: null, assetAllocations: null } } return { inputs, outputs, assetAllocations } } module.exports = { dustThreshold, finalize, finalizeAssets, inputBytes, outputBytes, sumOrNaN, sumForgiving, transactionBytes, uintOrNull, isNonAssetFunded }