diff --git a/bin/cli b/bin/cli index 77abe537..e5ef412c 100755 --- a/bin/cli +++ b/bin/cli @@ -310,7 +310,9 @@ CLI.prototype.sendTX = async function sendTX() { outputs: outputs, smart: this.config.bool('smart'), rate: this.config.ufixed('rate', 8), - subtractFee: this.config.bool('subtract-fee') + subtractFee: this.config.bool('subtract-fee'), + extraOutputs: this.config.uint('extra'), + selection: this.config.str('selection') }; const tx = await this.wallet.send(options); @@ -339,7 +341,9 @@ CLI.prototype.createTX = async function createTX() { outputs: [output], smart: this.config.bool('smart'), rate: this.config.ufixed('rate', 8), - subtractFee: this.config.bool('subtract-fee') + subtractFee: this.config.bool('subtract-fee'), + extraOutputs: this.config.uint('extra'), + selection: this.config.str('selection') }; const tx = await this.wallet.createTX(options); diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 286fb443..0f530e5d 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -1289,33 +1289,96 @@ MTX.prototype.fund = async function fund(coins, options) { // Do nothing. Change is added to fee. this.changeIndex = -1; assert.strictEqual(this.getFee(), select.fee + select.change); - } else { - this.outputs.push(change); - this.changeIndex = this.outputs.length - 1; - assert.strictEqual(this.getFee(), select.fee); + assert(select.fee + select.change <= CoinSelector.MAX_FEE); + return select; } + // Add a change output. + this.outputs.push(change); + this.changeIndex = this.outputs.length - 1; + + // Add more change outputs if we want. + // This is specifically designed to + // easily allow large hot wallets to + // bypass the stupidity of the 25 + // ancestor limit in the mempool. + // + // If core decides to add another + // ridiculous "spam prevention feature" + // like rejecting duplicate addresses, + // we can start making these addresses + // more dynamic. + if (select.extraOutputs > 0) + this.addExtra(select.extraOutputs); + + assert.strictEqual(this.getFee(), select.fee); + assert(select.fee <= CoinSelector.MAX_FEE); + return select; }; +/** + * Add extra change outputs. + * @param {Number} outputs + */ + +MTX.prototype.addExtra = function addExtra(outputs) { + assert(typeof outputs === 'number'); + assert(outputs >= 0); + + const index = this.changeIndex; + const output = this.outputs[index]; + assert(output); + + const change = output.value; + + outputs += 1; + + for (;;) { + assert(outputs !== 0); + + output.value = Math.floor(change / outputs); + + if (!output.isDust(policy.MIN_RELAY)) + break; + + outputs--; + } + + for (let i = 0; i < outputs - 1; i++) + this.outputs.push(output.clone()); + + const left = change - (outputs * output.value); + output.value += left; + + let total = 0; + + for (let i = index; i < this.outputs.length; i++) { + const output = this.outputs[i]; + total += output.value; + } + + assert.strictEqual(total, change); +}; + /** * Sort inputs and outputs according to BIP69. * @see https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki */ MTX.prototype.sortMembers = function sortMembers() { - let changeOutput = null; + let change = null; if (this.changeIndex !== -1) { - changeOutput = this.outputs[this.changeIndex]; - assert(changeOutput); + change = this.outputs[this.changeIndex]; + assert(change); } this.inputs.sort(sortInputs); this.outputs.sort(sortOutputs); - if (this.changeIndex !== -1) { - this.changeIndex = this.outputs.indexOf(changeOutput); + if (change) { + this.changeIndex = this.outputs.indexOf(change); assert(this.changeIndex !== -1); } }; @@ -1520,6 +1583,7 @@ function CoinSelector(tx, options) { this.maxFee = -1; this.round = false; this.changeAddress = null; + this.extraOutputs = 0; // Needed for size estimation. this.estimate = null; @@ -1629,6 +1693,13 @@ CoinSelector.prototype.fromOptions = function fromOptions(options) { } } + if (options.extraOutputs != null) { + assert(typeof options.extraOutputs === 'number'); + assert(options.extraOutputs >= 0); + assert(options.extraOutputs <= 100); + this.extraOutputs = options.extraOutputs; + } + if (options.estimate) { assert(typeof options.estimate === 'function'); this.estimate = options.estimate; @@ -1822,6 +1893,10 @@ CoinSelector.prototype.selectEstimate = async function selectEstimate() { this.tx.outputs.push(change); + // Extra change outputs. + for (let i = 0; i < this.extraOutputs; i++) + this.tx.outputs.push(change.clone()); + // Keep recalculating the fee and funding // until we reach some sort of equilibrium. do { diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 6f60391e..4549140a 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -390,6 +390,7 @@ HTTPServer.prototype.initRouter = function initRouter() { smart: valid.bool('smart'), subtractFee: valid.bool('subtractFee'), depth: valid.u32(['confirmations', 'depth']), + extraOutputs: valid.u32('extraOutputs'), outputs: [] }; @@ -428,6 +429,7 @@ HTTPServer.prototype.initRouter = function initRouter() { smart: valid.bool('smart'), subtractFee: valid.bool('subtractFee'), depth: valid.u32(['confirmations', 'depth']), + extraOutputs: valid.u32('extraOutputs'), outputs: [] }; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 4257e132..e8e743ac 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1397,6 +1397,7 @@ Wallet.prototype._fund = async function _fund(mtx, options) { hardFee: options.hardFee, subtractFee: options.subtractFee, changeAddress: account.change.getAddress(), + extraOutputs: options.extraOutputs, height: this.db.state.height, rate: rate, maxFee: options.maxFee,