3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 15:22:16 +00:00

Add emptyWallet option to SendRequest+tests and random typo fixes

This commit is contained in:
Matt Corallo 2013-07-22 11:22:01 +02:00 committed by Mike Hearn
parent 87ad8f2e5b
commit 1e69d2b0dd
2 changed files with 130 additions and 37 deletions

View File

@ -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<TransactionOutput> gathered;
public CoinSelection(BigInteger valueGathered, Set<TransactionOutput> gathered) {
public Collection<TransactionOutput> gathered;
public CoinSelection(BigInteger valueGathered, Collection<TransactionOutput> 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.</p>
*
* <p>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.</p>
* <p>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.</p>
*
* <p>You might also consider using a {@link SendRequest#fee} to set the fee added for the first kb of size.</p>
*/
@ -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.</p>
*
* <p>You MUST ensure that nanocoins is smaller than {@link Transaction#MIN_NONDUST_OUTPUT} or the transaction will
* <p>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.</p>
*
* @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,6 +1837,9 @@ 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<TransactionOutput> candidates = calculateSpendCandidates(true);
CoinSelection bestCoinSelection;
TransactionOutput bestChangeOutput = null;
if (!req.emptyWallet) {
// This can throw InsufficientMoneyException.
FeeCalculation feeCalculation;
try {
@ -1830,12 +1848,34 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
// TODO: Propagate this after 0.9 is released and stop returning a boolean.
return false;
}
CoinSelection bestCoinSelection = feeCalculation.bestCoinSelection;
TransactionOutput bestChangeOutput = feeCalculation.bestChangeOutput;
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);
}
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,6 +3234,13 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
private void resetTxInputs(SendRequest req, List<TransactionInput> 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) {
@ -3213,11 +3260,4 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
return size;
}
private void resetTxInputs(SendRequest req, List<TransactionInput> originalInputs) {
req.tx.clearInputs();
for (TransactionInput input : originalInputs)
req.tx.addInput(input);
}
}
}

View File

@ -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());
}
}