Skip to content

Commit 4d97480

Browse files
committed
wallet rbf: add new in/out if necessary
1 parent d5d302c commit 4d97480

File tree

4 files changed

+92
-25
lines changed

4 files changed

+92
-25
lines changed

lib/primitives/mtx.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1227,11 +1227,25 @@ class MTX extends TX {
12271227
async fund(coins, options) {
12281228
assert(options, 'Options are required.');
12291229
assert(options.changeAddress, 'Change address is required.');
1230-
assert(this.inputs.length === 0, 'TX is already funded.');
1230+
if (!options.bumpFee)
1231+
assert(this.inputs.length === 0, 'TX is already funded.');
12311232

12321233
// Select necessary coins.
12331234
const select = await this.selectCoins(coins, options);
12341235

1236+
// Bump Fee mode:
1237+
// We want the coin selector to ignore the values of
1238+
// all existing inputs and outputs, but still consider their size.
1239+
// Inside the coin selector this is done by making a copy of the TX
1240+
// and then setting the total output value to 0
1241+
// (input value is already ignored).
1242+
// The coin selector will add coins to cover the fee on the entire TX
1243+
// including paying for any inputs it adds.
1244+
// Now that coin selection is done, we add back in the fee paid by
1245+
// the original existing inputs and outputs so we can set the change value.
1246+
if (options.bumpFee)
1247+
select.fee += this.getFee();
1248+
12351249
// Add coins to transaction.
12361250
for (const coin of select.chosen)
12371251
this.addCoin(coin);

lib/wallet/coinselector.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class CoinSelector {
7171
this.changeAddress = null;
7272
this.inputs = new BufferMap();
7373
this.useSelectEstimate = false;
74+
this.bumpFee = false;
7475

7576
// Needed for size estimation.
7677
this.getAccount = null;
@@ -158,6 +159,11 @@ class CoinSelector {
158159
this.useSelectEstimate = options.useSelectEstimate;
159160
}
160161

162+
if (options.bumpFee != null) {
163+
assert(typeof options.bumpFee === 'boolean');
164+
this.bumpFee = options.bumpFee;
165+
}
166+
161167
if (options.changeAddress) {
162168
const addr = options.changeAddress;
163169
if (typeof addr === 'string') {
@@ -209,7 +215,7 @@ class CoinSelector {
209215

210216
async init(coins) {
211217
this.coins = coins.slice();
212-
this.outputValue = this.tx.getOutputValue();
218+
this.outputValue = this.bumpFee ? 0 : this.tx.getOutputValue();
213219
this.chosen = [];
214220
this.change = 0;
215221
this.fee = CoinSelector.MIN_FEE;

lib/wallet/wallet.js

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,8 @@ class Wallet extends EventEmitter {
11431143
rate: rate,
11441144
maxFee: options.maxFee,
11451145
useSelectEstimate: options.useSelectEstimate,
1146-
getAccount: this.getAccountByAddress.bind(this)
1146+
getAccount: this.getAccountByAddress.bind(this),
1147+
bumpFee: options.bumpFee
11471148
});
11481149

11491150
assert(mtx.getFee() <= CoinSelector.MAX_FEE, 'TX exceeds MAX_FEE.');
@@ -1376,27 +1377,42 @@ class Wallet extends EventEmitter {
13761377
}
13771378
}
13781379

1379-
if (!change)
1380-
throw new Error('Transaction has no change output.');
1381-
1382-
// Start by reducing absolute fee to 0
1383-
change.value += oldFee;
1384-
if (mtx.getFee() !== 0)
1385-
throw new Error('Arithmetic error for change.');
1386-
1387-
// Pay for replacement fees: BIP 125 rule #3
1388-
change.value -= oldFee;
1389-
1390-
// Pay for our own current fee: BIP 125 rule #4
1391-
change.value -= currentFee;
1392-
1393-
if (change.value < 0)
1394-
throw new Error('Change output insufficient for fee.');
1395-
1396-
// If change output is below dust, give it all to fee
1397-
if (change.isDust()) {
1398-
mtx.outputs.splice(mtx.changeIndex, 1);
1399-
mtx.changeIndex = -1;
1380+
if (change) {
1381+
// Start by reducing absolute fee to 0
1382+
change.value += oldFee;
1383+
if (mtx.getFee() !== 0)
1384+
throw new Error('Arithmetic error for change.');
1385+
1386+
// Pay for replacement fees: BIP 125 rule #3
1387+
change.value -= oldFee;
1388+
1389+
// Pay for our own current fee: BIP 125 rule #4
1390+
change.value -= currentFee;
1391+
1392+
// We need to add more inputs.
1393+
// This will obviously also fail the dust test next
1394+
// and conveniently remove the change output for us.
1395+
if (change.value < 0)
1396+
throw new Error('Change value insufficient to bump fee.');
1397+
1398+
// If change output is below dust,
1399+
// give it all to fee (remove change output)
1400+
if (change.isDust()) {
1401+
mtx.outputs.splice(mtx.changeIndex, 1);
1402+
mtx.changeIndex = -1;
1403+
}
1404+
} else {
1405+
// We need to add more inputs (and maybe a change output) to increase the
1406+
// fee. Since the original inputs and outputs already paid for
1407+
// their own fee (rule #3) all we have to do is pay for this
1408+
// new TX's fee (rule #4).
1409+
await this.fund(
1410+
mtx,
1411+
{
1412+
bumpFee: true, // set bump fee mode to ignore existing output values
1413+
rate, // rate in s/kvB for the rule 4 fee
1414+
depth: 1 // replacements can not add new unconfirmed coins
1415+
});
14001416
}
14011417

14021418
if (!sign)
@@ -1420,7 +1436,7 @@ class Wallet extends EventEmitter {
14201436
await this.wdb.send(ntx);
14211437
}
14221438

1423-
return ntx;
1439+
return mtx;
14241440
}
14251441

14261442
/**

test/wallet-rbf-test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,35 @@ describe('Wallet RBF', function () {
186186
});
187187
await node.rpc.generateToAddress([1, aliceReceive]);
188188
});
189+
190+
it('should bump a tx with no change by adding new in/out pair', async () => {
191+
const coins = await alice.getCoins();
192+
let coin;
193+
for (coin of coins) {
194+
if (!coin.coinbase)
195+
break;
196+
}
197+
const mtx = new MTX();
198+
mtx.addCoin(coin);
199+
mtx.addOutput(bobReceive, coin.value - 200);
200+
mtx.inputs[0].sequence = 0xfffffffd;
201+
await alice.sign(mtx);
202+
const tx = mtx.toTX();
203+
assert.strictEqual(tx.inputs.length, 1);
204+
assert.strictEqual(tx.outputs.length, 1);
205+
await alice.wdb.addTX(tx);
206+
await alice.wdb.send(tx);
207+
await forEvent(node.mempool, 'tx');
208+
209+
const rtx = await alice.bumpTXFee(tx.hash(), 2000 /* satoshis per kvB */, true, null);
210+
assert.strictEqual(rtx.inputs.length, 2);
211+
assert.strictEqual(rtx.outputs.length, 2);
212+
assert(rtx.getRate() >= 2000 && rtx.getRate() < 3000);
213+
214+
await forEvent(node.mempool, 'tx');
215+
assert(!node.mempool.hasEntry(tx.hash()));
216+
assert(node.mempool.hasEntry(rtx.hash()));
217+
218+
await node.rpc.generateToAddress([1, aliceReceive]);
219+
});
189220
});

0 commit comments

Comments
 (0)