mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2024-11-20 18:22:12 +01:00
Add method to clean up the wallet.
Currently, it just removes risky pending transaction from the wallet and only if their outputs have not been spent. Includes unit-tests by Miron Cuperman.
This commit is contained in:
parent
af1fdd4a14
commit
e7ea8483e4
@ -395,6 +395,18 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any of the outputs is marked as spent.
|
||||
*/
|
||||
public boolean isAnyOutputSpent() {
|
||||
maybeParse();
|
||||
for (TransactionOutput output : outputs) {
|
||||
if (!output.isAvailableForSpending())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if this transaction has at least one output that is owned by the given wallet and unspent, true
|
||||
* otherwise.
|
||||
|
@ -1425,6 +1425,40 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the wallet. Currently, it only removes risky pending transaction from the wallet and only if their
|
||||
* outputs have not been spent.
|
||||
*/
|
||||
public void cleanup() {
|
||||
lock.lock();
|
||||
try {
|
||||
boolean dirty = false;
|
||||
for (Iterator<Transaction> i = pending.values().iterator(); i.hasNext();) {
|
||||
Transaction tx = i.next();
|
||||
if (isTransactionRisky(tx, null) && !acceptRiskyTransactions) {
|
||||
log.debug("Found risky transaction {} in wallet during cleanup.", tx.getHashAsString());
|
||||
if (!tx.isAnyOutputSpent()) {
|
||||
tx.disconnectInputs();
|
||||
i.remove();
|
||||
transactions.remove(tx.getHash());
|
||||
dirty = true;
|
||||
log.info("Removed transaction {} from pending pool during cleanup.", tx.getHashAsString());
|
||||
} else {
|
||||
log.info(
|
||||
"Cannot remove transaction {} from pending pool during cleanup, as it's already spent partially.",
|
||||
tx.getHashAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dirty) {
|
||||
checkState(isConsistent());
|
||||
saveLater();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
EnumSet<Pool> getContainingPools(Transaction tx) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
@ -18,6 +18,8 @@ package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.core.Transaction.SigHash;
|
||||
import com.google.bitcoin.core.Wallet.SendRequest;
|
||||
import com.google.bitcoin.wallet.DefaultCoinSelector;
|
||||
import com.google.bitcoin.wallet.RiskAnalysis;
|
||||
import com.google.bitcoin.wallet.WalletTransaction;
|
||||
import com.google.bitcoin.wallet.WalletTransaction.Pool;
|
||||
import com.google.bitcoin.crypto.KeyCrypter;
|
||||
@ -128,6 +130,116 @@ public class WalletTest extends TestWithWallet {
|
||||
basicSpendingCommon(encryptedMixedWallet, myEncryptedAddress2, new ECKey().toAddress(params), true);
|
||||
}
|
||||
|
||||
static class TestRiskAnalysis implements RiskAnalysis {
|
||||
private final boolean risky;
|
||||
|
||||
public TestRiskAnalysis(boolean risky) {
|
||||
this.risky = risky;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result analyze() {
|
||||
return risky ? Result.NON_FINAL : Result.OK;
|
||||
}
|
||||
|
||||
public static class Analyzer implements RiskAnalysis.Analyzer {
|
||||
private final Transaction riskyTx;
|
||||
|
||||
Analyzer(Transaction riskyTx) {
|
||||
this.riskyTx = riskyTx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RiskAnalysis create(Wallet wallet, Transaction tx, List<Transaction> dependencies) {
|
||||
return new TestRiskAnalysis(tx == riskyTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class TestCoinSelector extends DefaultCoinSelector {
|
||||
@Override
|
||||
protected boolean shouldSelect(Transaction tx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private Transaction cleanupCommon(Address destination) throws Exception {
|
||||
receiveATransaction(wallet, myAddress);
|
||||
|
||||
BigInteger v2 = toNanoCoins(0, 50);
|
||||
SendRequest req = SendRequest.to(destination, v2);
|
||||
req.fee = toNanoCoins(0, 1);
|
||||
wallet.completeTx(req);
|
||||
|
||||
Transaction t2 = req.tx;
|
||||
|
||||
// Broadcast the transaction and commit.
|
||||
broadcastAndCommit(wallet, t2);
|
||||
|
||||
// At this point we have one pending and one spent
|
||||
|
||||
BigInteger v1 = toNanoCoins(0, 10);
|
||||
Transaction t = sendMoneyToWallet(wallet, v1, myAddress, null);
|
||||
Threading.waitForUserCode();
|
||||
sendMoneyToWallet(wallet, t, null);
|
||||
assertEquals("Wrong number of PENDING.4", 2, wallet.getPoolSize(Pool.PENDING));
|
||||
assertEquals("Wrong number of UNSPENT.4", 0, wallet.getPoolSize(Pool.UNSPENT));
|
||||
assertEquals("Wrong number of ALL.4", 3, wallet.getTransactions(true).size());
|
||||
assertEquals(toNanoCoins(0, 59), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
|
||||
// Now we have another incoming pending
|
||||
return t;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanup() throws Exception {
|
||||
Address destination = new ECKey().toAddress(params);
|
||||
Transaction t = cleanupCommon(destination);
|
||||
|
||||
// Consider the new pending as risky and remove it from the wallet
|
||||
wallet.setRiskAnalyzer(new TestRiskAnalysis.Analyzer(t));
|
||||
|
||||
wallet.cleanup();
|
||||
assertTrue(wallet.isConsistent());
|
||||
assertEquals("Wrong number of PENDING.5", 1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
|
||||
assertEquals("Wrong number of UNSPENT.5", 0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
|
||||
assertEquals("Wrong number of ALL.5", 2, wallet.getTransactions(true).size());
|
||||
assertEquals(toNanoCoins(0, 49), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupFailsDueToSpend() throws Exception {
|
||||
Address destination = new ECKey().toAddress(params);
|
||||
Transaction t = cleanupCommon(destination);
|
||||
|
||||
// Now we have another incoming pending. Spend everything.
|
||||
BigInteger v3 = toNanoCoins(0, 58);
|
||||
SendRequest req = SendRequest.to(destination, v3);
|
||||
|
||||
// Force selection of the incoming coin so that we can spend it
|
||||
req.coinSelector = new TestCoinSelector();
|
||||
|
||||
req.fee = toNanoCoins(0, 1);
|
||||
wallet.completeTx(req);
|
||||
wallet.commitTx(req.tx);
|
||||
|
||||
assertEquals("Wrong number of PENDING.5", 3, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
|
||||
assertEquals("Wrong number of UNSPENT.5", 0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
|
||||
assertEquals("Wrong number of ALL.5", 4, wallet.getTransactions(true).size());
|
||||
|
||||
// Consider the new pending as risky and try to remove it from the wallet
|
||||
wallet.setRiskAnalyzer(new TestRiskAnalysis.Analyzer(t));
|
||||
|
||||
wallet.cleanup();
|
||||
assertTrue(wallet.isConsistent());
|
||||
|
||||
// The removal should have failed
|
||||
assertEquals("Wrong number of PENDING.5", 3, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
|
||||
assertEquals("Wrong number of UNSPENT.5", 0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
|
||||
assertEquals("Wrong number of ALL.5", 4, wallet.getTransactions(true).size());
|
||||
assertEquals(toNanoCoins(0, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
}
|
||||
|
||||
private void basicSpendingCommon(Wallet wallet, Address toAddress, Address destination, boolean testEncryption) throws Exception {
|
||||
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. We
|
||||
// will attach a small fee. Because the Bitcoin protocol makes it difficult to determine the fee of an
|
||||
|
Loading…
Reference in New Issue
Block a user