diff --git a/core/src/main/java/org/bitcoinj/core/Wallet.java b/core/src/main/java/org/bitcoinj/core/Wallet.java index 74f9a3af..b60466b3 100644 --- a/core/src/main/java/org/bitcoinj/core/Wallet.java +++ b/core/src/main/java/org/bitcoinj/core/Wallet.java @@ -33,6 +33,7 @@ import org.bitcoinj.signers.*; import org.bitcoinj.store.*; import org.bitcoinj.utils.*; import org.bitcoinj.wallet.*; +import org.bitcoinj.wallet.KeyChain.KeyPurpose; import org.bitcoinj.wallet.Protos.Wallet.*; import org.bitcoinj.wallet.WalletTransaction.*; import org.slf4j.*; @@ -3835,6 +3836,32 @@ public class Wallet extends BaseTaggableObject return req; } + /** + * Construct a SendRequest for a CPFP (child-pays-for-parent) transaction. The resulting transaction is already + * completed, so you should directly proceed to signing and broadcasting/committing the transaction. CPFP is + * currently only supported by a few miners, so use with care. + */ + public static SendRequest childPaysForParent(Wallet wallet, Transaction parentTransaction, Coin feeRaise) { + TransactionOutput outputToSpend = null; + for (final TransactionOutput output : parentTransaction.getOutputs()) { + if (output.isMine(wallet) && output.isAvailableForSpending() + && output.getValue().isGreaterThan(feeRaise)) { + outputToSpend = output; + break; + } + } + // TODO spend another confirmed output of own wallet if needed + checkNotNull(outputToSpend, "Can't find adequately sized output that spends to us"); + + final Transaction tx = new Transaction(parentTransaction.getParams()); + tx.addInput(outputToSpend); + tx.addOutput(outputToSpend.getValue().subtract(feeRaise), wallet.freshAddress(KeyPurpose.CHANGE)); + tx.setPurpose(Transaction.Purpose.RAISE_FEE); + final SendRequest req = forTx(tx); + req.completed = true; + return req; + } + public static SendRequest toCLTVPaymentChannel(NetworkParameters params, Date releaseTime, ECKey from, ECKey to, Coin value) { long time = releaseTime.getTime() / 1000L; checkArgument(time >= Transaction.LOCKTIME_THRESHOLD, "Release time was too small"); diff --git a/core/src/test/java/org/bitcoinj/core/WalletTest.java b/core/src/test/java/org/bitcoinj/core/WalletTest.java index 7e6bb1bb..6283b156 100644 --- a/core/src/test/java/org/bitcoinj/core/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/core/WalletTest.java @@ -22,6 +22,7 @@ import org.bitcoinj.core.listeners.WalletCoinsReceivedEventListener; import org.bitcoinj.core.listeners.WalletCoinsSentEventListener; import org.bitcoinj.core.listeners.TransactionConfidenceEventListener; import org.bitcoinj.core.TransactionConfidence.ConfidenceType; +import org.bitcoinj.core.Wallet.BalanceType; import org.bitcoinj.core.Wallet.SendRequest; import org.bitcoinj.crypto.*; import org.bitcoinj.script.Script; @@ -3062,6 +3063,29 @@ public class WalletTest extends TestWithWallet { assertEquals(outputValue, request.tx.getOutput(0).getValue()); } + @Test + public void childPaysForParent() throws Exception { + // Receive confirmed balance to play with. + Transaction toMe = createFakeTxWithoutChangeAddress(PARAMS, COIN, myAddress); + wallet.receiveFromBlock(toMe, createFakeBlock(blockStore, toMe).storedBlock, + AbstractBlockChain.NewBlockType.BEST_CHAIN, 0); + assertEquals(Coin.COIN, wallet.getBalance(BalanceType.ESTIMATED_SPENDABLE)); + assertEquals(Coin.COIN, wallet.getBalance(BalanceType.AVAILABLE_SPENDABLE)); + // Receive unconfirmed coin without fee. + Transaction toMeWithoutFee = createFakeTxWithoutChangeAddress(PARAMS, COIN, myAddress); + wallet.receivePending(toMeWithoutFee, null); + assertEquals(Coin.COIN.multiply(2), wallet.getBalance(BalanceType.ESTIMATED_SPENDABLE)); + assertEquals(Coin.COIN, wallet.getBalance(BalanceType.AVAILABLE_SPENDABLE)); + // Craft a child-pays-for-parent transaction. + final Coin feeRaise = MILLICOIN; + final SendRequest sendRequest = SendRequest.childPaysForParent(wallet, toMeWithoutFee, feeRaise); + wallet.signTransaction(sendRequest); + wallet.commitTx(sendRequest.tx); + assertEquals(Transaction.Purpose.RAISE_FEE, sendRequest.tx.getPurpose()); + assertEquals(Coin.COIN.multiply(2).subtract(feeRaise), wallet.getBalance(BalanceType.ESTIMATED_SPENDABLE)); + assertEquals(Coin.COIN, wallet.getBalance(BalanceType.AVAILABLE_SPENDABLE)); + } + @Test public void keyRotationRandom() throws Exception { Utils.setMockClock();