diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index 83d293a9..c160ab84 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -24,6 +24,8 @@ import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptOpCodes; import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.wallet.WalletTransaction.Pool; + +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; @@ -636,6 +638,9 @@ public class Transaction extends ChildMessage implements Serializable { } s.append(String.format(Locale.US, " time locked until %s%n", time)); } + if (isOptInFullRBF()) { + s.append(" opts into full replace-by-fee%n"); + } if (inputs.size() == 0) { s.append(String.format(Locale.US, " INCOMPLETE: No inputs!%n")); return s.toString(); @@ -675,6 +680,10 @@ public class Transaction extends ChildMessage implements Serializable { s.append(Utils.HEX.encode(scriptPubKey.getPubKeyHash())); } } + String flags = Joiner.on(", ").skipNulls().join(in.hasSequence() ? "has sequence" : null, + in.isOptInFullRBF() ? "opts into full RBF" : null); + if (!flags.isEmpty()) + s.append("\n (").append(flags).append(')'); } catch (Exception e) { s.append("[exception: ").append(e.getMessage()).append("]"); } @@ -1272,6 +1281,17 @@ public class Transaction extends ChildMessage implements Serializable { return false; } + /** + * Returns whether this transaction will opt into the + * full replace-by-fee semantics. + */ + public boolean isOptInFullRBF() { + for (TransactionInput input : getInputs()) + if (input.isOptInFullRBF()) + return true; + return false; + } + /** *

Returns true if this transaction is considered finalized and can be placed in a block. Non-finalized * transactions won't be included by miners and can be replaced with newer versions using sequence numbers. diff --git a/core/src/main/java/org/bitcoinj/core/TransactionInput.java b/core/src/main/java/org/bitcoinj/core/TransactionInput.java index d32f990f..9b43f108 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionInput.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionInput.java @@ -22,6 +22,8 @@ import org.bitcoinj.wallet.DefaultRiskAnalysis; import org.bitcoinj.wallet.KeyBag; import org.bitcoinj.wallet.RedeemData; +import com.google.common.base.Joiner; + import javax.annotation.Nullable; import java.io.IOException; import java.io.ObjectOutputStream; @@ -48,7 +50,7 @@ public class TransactionInput extends ChildMessage implements Serializable { // Magic outpoint index that indicates the input is in fact unconnected. private static final long UNCONNECTED = 0xFFFFFFFFL; - // Allows for altering transactions after they were broadcast. + // Allows for altering transactions after they were broadcast. Values below NO_SEQUENCE-1 mean it can be altered. private long sequence; // Data needed to connect to the output of the transaction we're gathering coins from. private TransactionOutPoint outpoint; @@ -273,18 +275,6 @@ public class TransactionInput extends ChildMessage implements Serializable { return value; } - /** - * Returns a human readable debug string. - */ - @Override - public String toString() { - try { - return isCoinBase() ? "TxIn: COINBASE" : "TxIn for [" + outpoint + "]: " + getScriptSig(); - } catch (ScriptException e) { - throw new RuntimeException(e); - } - } - public enum ConnectionResult { NO_SUCH_TX, ALREADY_SPENT, @@ -409,6 +399,14 @@ public class TransactionInput extends ChildMessage implements Serializable { return sequence != NO_SEQUENCE; } + /** + * Returns whether this input will cause a transaction to opt into the + * full replace-by-fee semantics. + */ + public boolean isOptInFullRBF() { + return sequence < NO_SEQUENCE - 1; + } + /** * For a connected transaction, runs the script against the connected pubkey and verifies they are correct. * @throws ScriptException if the script did not verify. @@ -492,4 +490,26 @@ public class TransactionInput extends ChildMessage implements Serializable { result = 31 * result + (scriptSig != null ? scriptSig.hashCode() : 0); return result; } + + /** + * Returns a human readable debug string. + */ + @Override + public String toString() { + StringBuilder s = new StringBuilder("TxIn"); + try { + if (isCoinBase()) { + s.append(": COINBASE"); + } else { + s.append(" for [").append(outpoint).append("]: ").append(getScriptSig()); + String flags = Joiner.on(", ").skipNulls().join(hasSequence() ? "has sequence" : null, + isOptInFullRBF() ? "opts into full RBF" : null); + if (!flags.isEmpty()) + s.append(" (").append(flags).append(')'); + } + return s.toString(); + } catch (ScriptException e) { + throw new RuntimeException(e); + } + } } diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index 9fe4dfbc..5d3db9e2 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -10,6 +10,8 @@ import org.junit.Test; import org.easymock.EasyMock; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.replay; @@ -296,4 +298,14 @@ public class TransactionTest { tx.addSignedInput(fakeTx.getOutput(0).getOutPointFor(), script, key); } -} \ No newline at end of file + + @Test + public void optInFullRBF() { + // a standard transaction as wallets would create + Transaction tx = newTransaction(); + assertFalse(tx.isOptInFullRBF()); + + tx.getInputs().get(0).setSequenceNumber(TransactionInput.NO_SEQUENCE - 2); + assertTrue(tx.isOptInFullRBF()); + } +}