diff --git a/src/CONSTANTS.js b/src/CONSTANTS.js index fa3a0a9b..fa5236c4 100644 --- a/src/CONSTANTS.js +++ b/src/CONSTANTS.js @@ -27,8 +27,8 @@ const CONSTANTS = { UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR: 5, MAX_STANDARD_TX_SIZE: 100000, MAX_P2SH_SIGOPS: 15, - MAX_INPUTS_FOR_AUTO_IX: 4, - UTXO_MAX_INPUTS_PER_TX: 25, + // limit to how many times an unconfirmed input in a new tx can be respent + UTXO_CHAINED_SPENDING_LIMIT_FOR_TX: 25, FEES: { DUST_RELAY_TX_FEE: 1000, ZERO: 0, diff --git a/src/types/Account/Account.js b/src/types/Account/Account.js index 3e1354dd..5a2cfe47 100644 --- a/src/types/Account/Account.js +++ b/src/types/Account/Account.js @@ -5,7 +5,7 @@ const { WALLET_TYPES } = require('../../CONSTANTS'); const { is } = require('../../utils'); const EVENTS = require('../../EVENTS'); const Wallet = require('../Wallet/Wallet.js'); -const { simpleTransactionOptimizedAccumulator } = require('../../utils/coinSelections/strategies'); +const { simpleDescendingAccumulator } = require('../../utils/coinSelections/strategies'); function getNextUnusedAccountIndexForWallet(wallet) { if (wallet && wallet.accounts) { @@ -35,7 +35,7 @@ const defaultOptions = { plugins: [], injectDefaultPlugins: true, debug: false, - strategy: simpleTransactionOptimizedAccumulator, + strategy: simpleDescendingAccumulator, }; /* eslint-disable no-underscore-dangle */ diff --git a/src/utils/coinSelection.spec.js b/src/utils/coinSelection.spec.js index f231a254..11b6ed9c 100644 --- a/src/utils/coinSelection.spec.js +++ b/src/utils/coinSelection.spec.js @@ -3,6 +3,7 @@ const {Transaction, Address, Script} = require('@dashevo/dashcore-lib'); const coinSelection = require('./coinSelection'); const {utxosList} = require('../../fixtures/crackspice'); const STRATEGIES = require('./coinSelections/strategies'); +const TransactionEstimator = require('./coinSelections/TransactionEstimator') const utxosListAsUnspentOutput = utxosList.map((utxo)=> Transaction.UnspentOutput(utxo)); const outputs = { @@ -56,7 +57,6 @@ describe('Utils - coinSelection', function suite() { it('should require a outputsList with at least one output', () => { expect(() => coinSelection(utxosList, [])).to.throw('outputsList must contains at least 1 output'); }); - it('should require a outputsList with valid outputs', () => { expect(() => coinSelection(utxosListAsUnspentOutput, [{toto: true}])).to.throw('data parameter supplied is not a string.'); }); @@ -317,6 +317,51 @@ describe('Utils - coinSelection', function suite() { expect(result).to.deep.equal(expectedResult); }); + it('should handle externally crafted strategy', function () { + // A dummy strategy that takes a random selection of utxo + const externalStrategy = (utxosList, outputsList, deductFee = false, feeCategory = 'normal') => { + const copiedUtxos = [...utxosList]; + const txEstimator = new TransactionEstimator(feeCategory); + + txEstimator.addOutputs(outputsList); + + let inputValue = 0; + let outputValue = txEstimator.getOutValue(); + const randomlySelectedUtxos = []; + + while(inputValue { // const utxo = utxosList[15]; diff --git a/src/utils/coinSelections/index.js b/src/utils/coinSelections/index.js index 51406c82..9e3cebbe 100644 --- a/src/utils/coinSelections/index.js +++ b/src/utils/coinSelections/index.js @@ -4,7 +4,7 @@ const InvalidUTXO = require('../../errors/InvalidUTXO'); const InvalidOutput = require('../../errors/InvalidOutput'); const CoinSelectionUnsufficientUTXOS = require('../../errors/CoinSelectionUnsufficientUTXOS'); -module.exports = function coinSelection(utxosList, outputsList, deductFee = false, feeCategory = 'normal', strategy = STRATEGIES.simpleTransactionOptimizedAccumulator) { +module.exports = function coinSelection(utxosList, outputsList, deductFee = false, feeCategory = 'normal', strategy = STRATEGIES.simpleDescendingAccumulator) { if (!utxosList) { throw new Error('A utxosList is required'); } if (utxosList.constructor.name !== Array.name) { throw new Error('UtxosList is expected to be an array of utxos'); } if (utxosList.length < 1) { throw new Error('utxosList must contain at least 1 utxo'); } diff --git a/src/utils/coinSelections/strategies/index.js b/src/utils/coinSelections/strategies/index.js index 7ac9472e..d38fd2a7 100644 --- a/src/utils/coinSelections/strategies/index.js +++ b/src/utils/coinSelections/strategies/index.js @@ -1,10 +1,8 @@ const simpleAscendingAccumulator = require('./simpleAscendingAccumulator'); const simpleDescendingAccumulator = require('./simpleDescendingAccumulator'); -const simpleTransactionOptimizedAccumulator = require('./simpleTransactionOptimizedAccumulator'); const STRATEGIES = { simpleDescendingAccumulator, simpleAscendingAccumulator, - simpleTransactionOptimizedAccumulator, }; module.exports = STRATEGIES; diff --git a/src/utils/coinSelections/strategies/simpleTransactionOptimizedAccumulator.js b/src/utils/coinSelections/strategies/simpleTransactionOptimizedAccumulator.js deleted file mode 100644 index a46b22f8..00000000 --- a/src/utils/coinSelections/strategies/simpleTransactionOptimizedAccumulator.js +++ /dev/null @@ -1,82 +0,0 @@ -const { sortBy } = require('lodash'); -const TransactionEstimator = require('../TransactionEstimator.js'); -const { MAX_INPUTS_FOR_AUTO_IX } = require('../../../CONSTANTS'); -/** - * Given a utxos list and a threesholdSatoshis, will add them - * without any further logic up to met with requested params. - * @param utxos - * @param thresholdSatoshis - * @return {*} - */ -const simplyAccumulateUtxos = (utxos, thresholdSatoshis) => { - let pendingSatoshis = 0; - const accumulatedUtxos = utxos.filter((utxo) => { - if (pendingSatoshis < thresholdSatoshis) { - pendingSatoshis += utxo.satoshis; - return utxo; - } - return false; - }); - if (pendingSatoshis < thresholdSatoshis) { - throw new Error('Unsufficient utxo amount'); - } - return accumulatedUtxos; -}; -/** - * Simple transaction optimized accumulator strategy : - * Will try to not exceed the simpleTransaction definition by selecting 4 or less inputs - * in ascending order - * @param {*} utxosList - A utxos list - * @param {*} outputsList - The output list - * @param {*} deductFee - default: false - Deduct fee from outputs - * @param {*} feeCategory - default: normal - - */ -const simpleTransactionOptimizedAccumulator = (utxosList, outputsList, deductFee = false, feeCategory = 'normal') => { - const txEstimator = new TransactionEstimator(feeCategory); - - // We add our outputs, theses will change only in case deductfee being true - txEstimator.addOutputs(outputsList); - - const sortedUtxosList = sortBy(utxosList, ['-satoshis', 'txid', 'outputIndex']); - - const totalOutputValue = txEstimator.getTotalOutputValue(); - - let simplyAccumulatedUtxos = simplyAccumulateUtxos(sortedUtxosList, totalOutputValue); - let len = simplyAccumulatedUtxos.length; - - if (simplyAccumulatedUtxos.length > MAX_INPUTS_FOR_AUTO_IX) { - for (let i = 0; i < sortedUtxosList.length; i += 1) { - simplyAccumulatedUtxos = sortedUtxosList.slice(i); - len = simplyAccumulatedUtxos.length; - if (len <= MAX_INPUTS_FOR_AUTO_IX) break; - } - } - - - // We add the expected inputs, which should match the requested amount - // TODO : handle case when we do not match it. - txEstimator.addInputs(simplyAccumulatedUtxos); - - const estimatedFee = txEstimator.getFeeEstimate(); - if (deductFee === true) { - // Then we check that we will be able to do it - const inValue = txEstimator.getInValue(); - const outValue = txEstimator.getOutValue(); - if (inValue < outValue + estimatedFee) { - // We don't have enough change for fee, so we remove from outValue - txEstimator.reduceFeeFromOutput((outValue + estimatedFee) - inValue); - } else { - // TODO : Here we can add some process to check up that we clearly have enough to deduct fee - } - } - // console.log('estimatedFee are', estimatedFee, 'satoshis'); - return { - utxos: txEstimator.getInputs(), - outputs: txEstimator.getOutputs(), - feeCategory, - estimatedFee, - utxosValue: txEstimator.getInValue(), - }; -}; -module.exports = simpleTransactionOptimizedAccumulator; diff --git a/src/utils/coinSelections/strategies/simpleTransactionOptimizedAccumulator.spec.js b/src/utils/coinSelections/strategies/simpleTransactionOptimizedAccumulator.spec.js deleted file mode 100644 index 2541bcaa..00000000 --- a/src/utils/coinSelections/strategies/simpleTransactionOptimizedAccumulator.spec.js +++ /dev/null @@ -1,180 +0,0 @@ -const { expect } = require('chai'); -const { simpleTransactionOptimizedAccumulator } = require('./index'); -const getUTXOS = require('../../../types/Account/methods/getUTXOS'); -const duringDevelopStore = require('../../../../fixtures/duringdevelop-fullstore-snapshot-1549310417'); - -console.error('coinSelection.strategies.simpleTransactionOptimizedAccumulator needs a rebuilt store'); -describe.skip('CoinSelection - Strategy - simpleTransactionOptimizedAccumulator', () => { - it('should work as expected', () => { - const self = { - }; - - const utxosList = getUTXOS.call({ - store: duringDevelopStore, - getStore: () => this.store, - walletId: '5061b8276c', - }); - - const outputsList1e4 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e4 }]; - const outputsList1e5 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e5 }]; - const outputsList1e6 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e6 }]; - const outputsList1e7 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e7 }]; - const outputsList1e8 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e8 }]; - const outputsList1e9 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e9 }]; - const outputsList1e10 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e10 }]; - const outputsList2e10 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 2e10 }]; - const outputsList6e10 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 6e10 }]; - const outputsList999e8 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 999.99998628e8 }]; - const outputsList1e11 = [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1e11 }]; - - const expectedRes1e4 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 10000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes1e5 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 100000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes1e6 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes1e7 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 10000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes1e8 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 100000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes1e9 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 1000000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes1e10 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 10000000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 205, - utxosValue: 10000000000, - }; - const expectedRes2e10 = { - utxos: [{ - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }, { - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 20000000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 350, - utxosValue: 20000000000, - }; - const expectedRes6e10 = { - utxos: [{ - satoshis: 1000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }, { - satoshis: 100, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }, { - satoshis: 1088887528, script: '76a9144f8aa6c3e302911b8c6b0ecb0538d209c144f84988ac', - }, { - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 60000000000, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 640, - utxosValue: 11088888628, - }; - const expectedRes999e8 = { - utxos: [{ - satoshis: 1000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }, { - satoshis: 100, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }, { - satoshis: 1088887528, script: '76a9144f8aa6c3e302911b8c6b0ecb0538d209c144f84988ac', - }, { - satoshis: 10000000000, script: '76a9143a9202121ee9ef906e567101326f2ecf8ad4ecbc88ac', - }], - outputs: [{ address: 'yU7sNM4j6fzKtbah24gCXdN636piQN8F2f', satoshis: 99999998628, scriptType: 'P2PKH' }], - feeCategory: 'normal', - estimatedFee: 640, - utxosValue: 11088888628, - }; - - - const res1e4 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e4); - const res1e5 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e5); - const res1e6 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e6); - const res1e7 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e7); - const res1e8 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e8); - const res1e9 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e9); - const res1e10 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e10); - const res2e10 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList2e10); - const res6e10 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList6e10); - const res999e8 = simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList999e8); - - res1e4.utxos[0] = res1e4.utxos[0].toJSON(); - expect(res1e4).to.deep.equal(expectedRes1e4); - - res1e5.utxos[0] = res1e5.utxos[0].toJSON(); - expect(res1e5).to.deep.equal(expectedRes1e5); - - res1e6.utxos[0] = res1e6.utxos[0].toJSON(); - expect(res1e6).to.deep.equal(expectedRes1e6); - - res1e7.utxos[0] = res1e7.utxos[0].toJSON(); - expect(res1e7).to.deep.equal(expectedRes1e7); - - res1e8.utxos[0] = res1e8.utxos[0].toJSON(); - expect(res1e8).to.deep.equal(expectedRes1e8); - - res1e9.utxos[0] = res1e9.utxos[0].toJSON(); - expect(res1e9).to.deep.equal(expectedRes1e9); - - res1e10.utxos[0] = res1e10.utxos[0].toJSON(); - expect(res1e10).to.deep.equal(expectedRes1e10); - - res2e10.utxos[0] = res2e10.utxos[0].toJSON(); - res2e10.utxos[1] = res2e10.utxos[1].toJSON(); - expect(res2e10).to.deep.equal(expectedRes2e10); - - res6e10.utxos = res6e10.utxos.map((utxo) => utxo.toJSON()); - expect(res6e10).to.deep.equal(expectedRes6e10); - - res999e8.utxos = res999e8.utxos.map((utxo) => utxo.toJSON()); - expect(res999e8).to.deep.equal(expectedRes999e8); - - expect(() => simpleTransactionOptimizedAccumulator.call(self, utxosList, outputsList1e11)).to.throw(('Unsufficient utxo amount')); - }); -});