diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java
index e0facf80..c8c0d508 100644
--- a/core/src/main/java/com/google/bitcoin/core/Wallet.java
+++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java
@@ -142,8 +142,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
/** Represents the results of a {@link CoinSelector#select(java.math.BigInteger, java.util.LinkedList)} operation */
public static class CoinSelection {
public BigInteger valueGathered;
- public Set gathered;
- public CoinSelection(BigInteger valueGathered, Set gathered) {
+ public Collection gathered;
+ public CoinSelection(BigInteger valueGathered, Collection gathered) {
this.valueGathered = valueGathered;
this.gathered = gathered;
}
@@ -1523,6 +1523,13 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
*/
public Transaction tx;
+ /**
+ * When emptyWallet is set, all available coins are sent to the first output in tx (its value is ignored and set
+ * to {@link com.google.bitcoin.core.Wallet#getBalance()} - the fees required for the transaction). Any
+ * additional outputs are removed.
+ */
+ public boolean emptyWallet = false;
+
/**
* "Change" means the difference between the value gathered by a transactions inputs (the size of which you
* don't really control as it depends on who sent you money), and the value being sent somewhere else. The
@@ -1556,10 +1563,10 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
* a way for people to prioritize their transactions over others and is used as a way to make denial of service
* attacks expensive.
*
- * This is a dynamic fee (in satoshis) which will be added to the transaction for each kilobyte in size after
- * the first. This is useful as as miners usually sort pending transactions by their fee per unit size when
- * choosing which transactions to add to a block. Note that, to keep this equivalent to the reference client
- * definition, a kilobyte is defined as 1000 bytes, not 1024.
+ * This is a dynamic fee (in satoshis) which will be added to the transaction for each kilobyte in size
+ * including the first. This is useful as as miners usually sort pending transactions by their fee per unit size
+ * when choosing which transactions to add to a block. Note that, to keep this equivalent to the reference
+ * client definition, a kilobyte is defined as 1000 bytes, not 1024.
*
* You might also consider using a {@link SendRequest#fee} to set the fee added for the first kb of size.
*/
@@ -1628,6 +1635,14 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
req.tx = tx;
return req;
}
+
+ public static SendRequest emptyWallet(Address destination) {
+ SendRequest req = new SendRequest();
+ req.tx = new Transaction(destination.getParameters());
+ req.tx.addOutput(BigInteger.ZERO, destination);
+ req.emptyWallet = true;
+ return req;
+ }
}
/**
@@ -1649,7 +1664,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
* prevent this, but that should only occur once the transaction has been accepted by the network. This implies
* you cannot have more than one outstanding sending tx at once.
*
- * You MUST ensure that nanocoins is smaller than {@link Transaction#MIN_NONDUST_OUTPUT} or the transaction will
+ *
You MUST ensure that nanocoins is larger than {@link Transaction#MIN_NONDUST_OUTPUT} or the transaction will
* almost certainly be rejected by the network as dust.
*
* @param address The Bitcoin address to send the money to.
@@ -1801,7 +1816,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
// We need to know if we need to add an additional fee because one of our values are smaller than 0.01 BTC
boolean needAtLeastReferenceFee = false;
- if (req.ensureMinRequiredFee) {
+ if (req.ensureMinRequiredFee && !req.emptyWallet) { // min fee checking is handled later for emptyWallet
for (TransactionOutput output : req.tx.getOutputs())
if (output.getValue().compareTo(Utils.CENT) < 0) {
if (output.getValue().compareTo(output.getMinNonDustValue()) < 0) {
@@ -1822,20 +1837,45 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
// Note that output.isMine(this) needs to test the keychain which is currently an array, so it's
// O(candidate outputs ^ keychain.size())! There's lots of low hanging fruit here.
LinkedList candidates = calculateSpendCandidates(true);
- // This can throw InsufficientMoneyException.
- FeeCalculation feeCalculation;
- try {
- feeCalculation = new FeeCalculation(req, value, originalInputs, needAtLeastReferenceFee, candidates);
- } catch (InsufficientMoneyException e) {
- // TODO: Propagate this after 0.9 is released and stop returning a boolean.
- return false;
+ CoinSelection bestCoinSelection;
+ TransactionOutput bestChangeOutput = null;
+ if (!req.emptyWallet) {
+ // This can throw InsufficientMoneyException.
+ FeeCalculation feeCalculation;
+ try {
+ feeCalculation = new FeeCalculation(req, value, originalInputs, needAtLeastReferenceFee, candidates);
+ } catch (InsufficientMoneyException e) {
+ // TODO: Propagate this after 0.9 is released and stop returning a boolean.
+ return false;
+ }
+ bestCoinSelection = feeCalculation.bestCoinSelection;
+ bestChangeOutput = feeCalculation.bestChangeOutput;
+ } else {
+ BigInteger valueGathered = BigInteger.ZERO;
+ for (TransactionOutput output : candidates)
+ valueGathered = valueGathered.add(output.getValue());
+ bestCoinSelection = new CoinSelection(valueGathered, candidates);
+ req.tx.getOutput(0).setValue(valueGathered);
}
- CoinSelection bestCoinSelection = feeCalculation.bestCoinSelection;
- TransactionOutput bestChangeOutput = feeCalculation.bestChangeOutput;
for (TransactionOutput output : bestCoinSelection.gathered)
req.tx.addInput(output);
+ if (req.ensureMinRequiredFee && req.emptyWallet) {
+ TransactionOutput output = req.tx.getOutput(0);
+ // Check if we need additional fee due to the transaction's size
+ int size = req.tx.bitcoinSerialize().length;
+ size += estimateBytesForSigning(bestCoinSelection);
+ BigInteger fee = (req.fee == null ? BigInteger.ZERO : req.fee)
+ .add(BigInteger.valueOf((size / 1000) + 1).multiply(req.feePerKb == null ? BigInteger.ZERO : req.feePerKb));
+ output.setValue(output.getValue().subtract(fee));
+ // Check if we need additional fee due to the output's value
+ if (output.getValue().compareTo(Utils.CENT) < 0 && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0)
+ output.setValue(output.getValue().subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fee)));
+ if (output.getMinNonDustValue().compareTo(output.getValue()) > 0)
+ return false;
+ }
+
totalInput = totalInput.add(bestCoinSelection.valueGathered);
if (bestChangeOutput != null) {
@@ -3194,30 +3234,30 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
- private int estimateBytesForSigning(CoinSelection selection) {
- int size = 0;
- for (TransactionOutput output : selection.gathered) {
- try {
- if (output.getScriptPubKey().isSentToAddress()) {
- // Send-to-address spends usually take maximum pubkey.length (as it may be compressed or not) + 75 bytes
- size += findKeyFromPubHash(output.getScriptPubKey().getPubKeyHash()).getPubKey().length + 75;
- } else if (output.getScriptPubKey().isSentToRawPubKey())
- size += 74; // Send-to-pubkey spends usually take maximum 74 bytes to spend
- else
- throw new RuntimeException("Unknown output type returned in coin selection");
- } catch (ScriptException e) {
- // If this happens it means an output script in a wallet tx could not be understood. That should never
- // happen, if it does it means the wallet has got into an inconsistent state.
- throw new RuntimeException(e);
- }
- }
- return size;
- }
-
private void resetTxInputs(SendRequest req, List originalInputs) {
req.tx.clearInputs();
for (TransactionInput input : originalInputs)
req.tx.addInput(input);
}
}
+
+ private int estimateBytesForSigning(CoinSelection selection) {
+ int size = 0;
+ for (TransactionOutput output : selection.gathered) {
+ try {
+ if (output.getScriptPubKey().isSentToAddress()) {
+ // Send-to-address spends usually take maximum pubkey.length (as it may be compressed or not) + 75 bytes
+ size += findKeyFromPubHash(output.getScriptPubKey().getPubKeyHash()).getPubKey().length + 75;
+ } else if (output.getScriptPubKey().isSentToRawPubKey())
+ size += 74; // Send-to-pubkey spends usually take maximum 74 bytes to spend
+ else
+ throw new RuntimeException("Unknown output type returned in coin selection");
+ } catch (ScriptException e) {
+ // If this happens it means an output script in a wallet tx could not be understood. That should never
+ // happen, if it does it means the wallet has got into an inconsistent state.
+ throw new RuntimeException(e);
+ }
+ }
+ return size;
+ }
}
diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
index 5aaae972..846e09a3 100644
--- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
@@ -46,6 +46,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static com.google.bitcoin.core.TestUtils.*;
+import static com.google.bitcoin.core.TestUtils.makeSolvedTestBlock;
import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;
import static com.google.bitcoin.core.Utils.toNanoCoins;
import static org.junit.Assert.*;
@@ -1825,4 +1826,56 @@ public class WalletTest extends TestWithWallet {
Threading.waitForUserCode();
assertEquals(1, flag.get());
}
+
+ @Test
+ public void testEmptyRandomWallet() throws Exception {
+ // Add a random set of outputs
+ StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, new ECKey().toAddress(params)), BigInteger.ONE, 1);
+ Random rng = new Random();
+ for (int i = 0; i < rng.nextInt(100) + 1; i++) {
+ Transaction tx = createFakeTx(params, BigInteger.valueOf(rng.nextInt((int) Utils.COIN.longValue())), myAddress);
+ wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
+ }
+ SendRequest request = SendRequest.emptyWallet(new ECKey().toAddress(params));
+ assertTrue(wallet.completeTx(request));
+ wallet.commitTx(request.tx);
+ assertEquals(BigInteger.ZERO, wallet.getBalance());
+ }
+
+ @Test
+ public void testEmptyWallet() throws Exception {
+ Address outputKey = new ECKey().toAddress(params);
+ // Add exactly 0.01
+ StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, outputKey), BigInteger.ONE, 1);
+ Transaction tx = createFakeTx(params, Utils.CENT, myAddress);
+ wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
+ SendRequest request = SendRequest.emptyWallet(outputKey);
+ assertTrue(wallet.completeTx(request));
+ wallet.commitTx(request.tx);
+ assertEquals(BigInteger.ZERO, wallet.getBalance());
+ assertEquals(Utils.CENT, request.tx.getOutput(0).getValue());
+
+ // Add just under 0.01
+ StoredBlock block2 = new StoredBlock(block.getHeader().createNextBlock(outputKey), BigInteger.ONE, 2);
+ tx = createFakeTx(params, Utils.CENT.subtract(BigInteger.ONE), myAddress);
+ wallet.receiveFromBlock(tx, block2, AbstractBlockChain.NewBlockType.BEST_CHAIN);
+ request = SendRequest.emptyWallet(outputKey);
+ assertTrue(wallet.completeTx(request));
+ wallet.commitTx(request.tx);
+ assertEquals(BigInteger.ZERO, wallet.getBalance());
+ assertEquals(Utils.CENT.subtract(BigInteger.ONE).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), request.tx.getOutput(0).getValue());
+
+ // Add an unsendable value
+ StoredBlock block3 = new StoredBlock(block2.getHeader().createNextBlock(outputKey), BigInteger.ONE, 3);
+ BigInteger outputValue = Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE);
+ tx = createFakeTx(params, outputValue, myAddress);
+ wallet.receiveFromBlock(tx, block3, AbstractBlockChain.NewBlockType.BEST_CHAIN);
+ request = SendRequest.emptyWallet(outputKey);
+ assertFalse(wallet.completeTx(request));
+ request.ensureMinRequiredFee = false;
+ assertTrue(wallet.completeTx(request));
+ wallet.commitTx(request.tx);
+ assertEquals(BigInteger.ZERO, wallet.getBalance());
+ assertEquals(outputValue, request.tx.getOutput(0).getValue());
+ }
}