package com.mygeopay.core.wallet;
import com.mygeopay.core.coins.CoinType;
import com.mygeopay.core.coins.Value;
import com.mygeopay.core.coins.ValueType;
import com.mygeopay.core.network.AddressStatus;
import com.mygeopay.core.network.BlockHeader;
import com.mygeopay.core.network.ServerClient;
import com.mygeopay.core.network.interfaces.BlockchainConnection;
import com.mygeopay.core.network.interfaces.TransactionEventListener;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.ScriptException;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.utils.ListenerRegistration;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.WalletTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import static com.mygeopay.core.Preconditions.checkNotNull;
import static com.mygeopay.core.Preconditions.checkState;
/**
* @author John L. Jegutanis
*/
abstract public class TransactionWatcherWallet implements WalletAccount {
private static final Logger log = LoggerFactory.getLogger(TransactionWatcherWallet.class);
private final static int TX_DEPTH_SAVE_THRESHOLD = 4;
final ReentrantLock lock = Threading.lock("TransactionWatcherWallet");
protected final CoinType coinType;
@Nullable private Sha256Hash lastBlockSeenHash;
private int lastBlockSeenHeight = -1;
private long lastBlockSeenTimeSecs = 0;
// Holds the status of every address we are watching. When connecting to the server, if we get a
// different status for a particular address this means that there are new transactions for that
// address and we have to fetch them. The status String could be null when an address is unused.
@VisibleForTesting
final HashMap<Address, String> addressesStatus;
@VisibleForTesting final transient ArrayList<Address> addressesSubscribed;
@VisibleForTesting final transient ArrayList<Address> addressesPendingSubscription;
@VisibleForTesting final transient HashMap<Address, AddressStatus> statusPendingUpdates;
@VisibleForTesting final transient HashSet<Sha256Hash> fetchingTransactions;
// The various pools below give quick access to wallet-relevant transactions by the state they're in:
//
// Pending: Transactions that didn't make it into the best chain yet. Pending transactions can be killed if a
// double-spend against them appears in the best chain, in which case they move to the dead pool.
// If a double-spend appears in the pending state as well, currently we just ignore the second
// and wait for the miners to resolve the race.
// Unspent: Transactions that appeared in the best chain and have outputs we can spend. Note that we store the
// entire transaction in memory even though for spending purposes we only really need the outputs, the
// reason being that this simplifies handling of re-orgs. It would be worth fixing this in future.
// Spent: Transactions that appeared in the best chain but don't have any spendable outputs. They're stored here
// for history browsing/auditing reasons only and in future will probably be flushed out to some other
// kind of cold storage or just removed.
// Dead: Transactions that we believe will never confirm get moved here, out of pending. Note that the Satoshi
// client has no notion of dead-ness: the assumption is that double spends won't happen so there's no
// need to notify the user about them. We take a more pessimistic approach and try to track the fact that
// transactions have been double spent so applications can do something intelligent (cancel orders, show
// to the user in the UI, etc). A transaction can leave dead and move into spent/unspent if there is a
// re-org to a chain that doesn't include the double spend.
@VisibleForTesting final Map<Sha256Hash, Transaction> pending;
@VisibleForTesting final Map<Sha256Hash, Transaction> unspent;
@VisibleForTesting final Map<Sha256Hash, Transaction> spent;
@VisibleForTesting final Map<Sha256Hash, Transaction> dead;
// All transactions together.
protected final Map<Sha256Hash, Transaction> transactions;
private BlockchainConnection blockchainConnection;
private List<ListenerRegistration<WalletAccountEventListener>> listeners;
// Wallet that this account belongs
@Nullable private transient Wallet wallet = null;
private Runnable saveLaterRunnable = new Runnable() {
@Override
public void run() {
if (wallet != null) wallet.saveLater();
}
};
private Runnable saveNowRunnable = new Runnable() {
@Override
public void run() {
if (wallet != null) wallet.saveNow();
}
};
// Constructor
public TransactionWatcherWallet(CoinType coinType) {
this.coinType = coinType;
addressesStatus = new HashMap<Address, String>();
addressesSubscribed = new ArrayList<Address>();
addressesPendingSubscription = new ArrayList<Address>();
statusPendingUpdates = new HashMap<Address, AddressStatus>();
fetchingTransactions = new HashSet<Sha256Hash>();
unspent = new HashMap<Sha256Hash, Transaction>();
spent = new HashMap<Sha256Hash, Transaction>();
pending = new HashMap<Sha256Hash, Transaction>();
dead = new HashMap<Sha256Hash, Transaction>();
transactions = new HashMap<Sha256Hash, Transaction>();
listeners = new CopyOnWriteArrayList<ListenerRegistration<WalletAccountEventListener>>();
}
@Override
public boolean isType(WalletAccount other) {
return other != null && coinType.equals(other.getCoinType());
}
@Override
public boolean isType(ValueType otherType) {
return otherType != null && coinType.equals(otherType);
}
@Override
public boolean isType(Address address) {
return address != null && coinType.equals(address.getParameters());
}
@Override
public CoinType getCoinType() {
return coinType;
}
@Override
public boolean isNew() {
return unspent.size() + spent.size() + pending.size() == 0;
}
public void setWallet(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
// Util
@Override
public void walletSaveLater() {
// Save in another thread to avoid cyclic locking of Wallet and WalletPocket
Threading.USER_THREAD.execute(saveLaterRunnable);
}
@Override
public void walletSaveNow() {
// Save in another thread to avoid cyclic locking of Wallet and WalletPocket
Threading.USER_THREAD.execute(saveNowRunnable);
}
/**
* Returns a set of all transactions in the wallet.
* @param includeDead If true, transactions that were overridden by a double spend are included.
*/
public Set<Transaction> getTransactions(boolean includeDead) {
lock.lock();
try {
Set<Transaction> all = new HashSet<Transaction>();
all.addAll(unspent.values());
all.addAll(spent.values());
all.addAll(pending.values());
if (includeDead)
all.addAll(dead.values());
return all;
} finally {
lock.unlock();
}
}
/**
* Returns a set of all WalletTransactions in the wallet.
*/
public Iterable<WalletTransaction> getWalletTransactions() {
lock.lock();
try {
Set<WalletTransaction> all = new HashSet<WalletTransaction>();
addWalletTransactionsToSet(all, WalletTransaction.Pool.UNSPENT, unspent.values());
addWalletTransactionsToSet(all, WalletTransaction.Pool.SPENT, spent.values());
addWalletTransactionsToSet(all, WalletTransaction.Pool.DEAD, dead.values());
addWalletTransactionsToSet(all, WalletTransaction.Pool.PENDING, pending.values());
return all;
} finally {
lock.unlock();
}
}
/**
* Just adds the transaction to a pool without doing anything else
* @param pool
* @param tx
*/
private void simpleAddTransaction(WalletTransaction.Pool pool, Transaction tx) {
lock.lock();
try {
transactions.put(tx.getHash(), tx);
switch (pool) {
case UNSPENT:
checkState(unspent.put(tx.getHash(), tx) == null);
break;
case SPENT:
checkState(spent.put(tx.getHash(), tx) == null);
break;
case PENDING:
checkState(pending.put(tx.getHash(), tx) == null);
break;
case DEAD:
checkState(dead.put(tx.getHash(), tx) == null);
break;
default:
throw new RuntimeException("Unknown wallet transaction type " + pool);
}
} finally {
lock.unlock();
}
}
private static void addWalletTransactionsToSet(Set<WalletTransaction> txs,
WalletTransaction.Pool poolType, Collection<Transaction> pool) {
for (Transaction tx : pool) {
txs.add(new WalletTransaction(poolType, tx));
}
}
/**
* Adds a transaction that has been associated with a particular wallet pool. This is intended for usage by
* deserialization code, such as the {@link WalletPocketProtobufSerializer} class. It isn't normally useful for
* applications. It does not trigger auto saving.
*/
public void addWalletTransaction(WalletTransaction wtx) {
lock.lock();
try {
addWalletTransaction(wtx.getPool(), wtx.getTransaction(), true);
} finally {
lock.unlock();
}
}
/**
* Marks outputs as spent, if we don't have the keys
*/
private void markNotOwnOutputs(Transaction transaction) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
for (TransactionOutput txo : transaction.getOutputs()) {
if (txo.isAvailableForSpending()) {
// We don't have keys for this txo therefore it is not ours
try {
if (findKeyFromPubHash(txo.getScriptPubKey().getPubKeyHash()) == null) {
txo.markAsSpent(null);
}
} catch (ScriptException ignore) {
// If we don't understand this output, don't use it
txo.markAsSpent(null);
}
}
}
}
/**
* Adds the given transaction to the given pools and registers a confidence change listener on it.
*/
private void addWalletTransaction(WalletTransaction.Pool pool, Transaction tx, boolean save) {
lock.lock();
try {
if (log.isInfoEnabled()) {
log.info("Adding {} tx {} to {}",
tx.isEveryOwnedOutputSpent(this) ? WalletTransaction.Pool.SPENT : WalletTransaction.Pool.UNSPENT, tx.getHash(), pool);
if (!tx.isEveryOwnedOutputSpent(this)) {
for (TransactionOutput transactionOutput : tx.getOutputs()) {
log.info("|- {} txo index {}",
transactionOutput.isAvailableForSpending() ? WalletTransaction.Pool.UNSPENT : WalletTransaction.Pool.SPENT,
transactionOutput.getIndex());
}
}
}
simpleAddTransaction(pool, tx);
markNotOwnOutputs(tx);
connectTransaction(tx);
queueOnNewBalance();
} finally {
lock.unlock();
}
// This is safe even if the listener has been added before, as TransactionConfidence ignores duplicate
// registration requests. That makes the code in the wallet simpler.
// TODO add txConfidenceListener
// tx.getConfidence().addEventListener(txConfidenceListener, Threading.SAME_THREAD);
if (save) walletSaveLater();
}
/**
* Returns a transaction object given its hash, if it exists in this wallet, or null otherwise.
*/
@Nullable
public Transaction getTransaction(String transactionId) {
return getTransaction(new Sha256Hash(transactionId));
}
/**
* Returns a transaction object given its hash, if it exists in this wallet, or null otherwise.
*/
@Nullable
public Transaction getTransaction(Sha256Hash hash) {
lock.lock();
try {
return transactions.get(hash);
} finally {
lock.unlock();
}
}
/**
* Returns transactions that match the hashes, some transactions could be missing.
*/
public HashMap<Sha256Hash, Transaction> getTransactions(HashSet<Sha256Hash> hashes) {
lock.lock();
try {
HashMap<Sha256Hash, Transaction> txs = new HashMap<Sha256Hash, Transaction>();
for (Sha256Hash hash : hashes) {
if (transactions.containsKey(hash)) {
txs.put(hash, transactions.get(hash));
}
}
return txs;
} finally {
lock.unlock();
}
}
/**
* Deletes transactions which appeared above the given block height from the wallet, but does not touch the keys.
* This is useful if you have some keys and wish to replay the block chain into the wallet in order to pick them up.
* Triggers auto saving.
*/
@Override
public void refresh() {
lock.lock();
try {
log.info("Refreshing wallet pocket {}", coinType);
lastBlockSeenHash = null;
lastBlockSeenHeight = -1;
lastBlockSeenTimeSecs = 0;
unspent.clear();
spent.clear();
pending.clear();
dead.clear();
transactions.clear();
addressesStatus.clear();
clearTransientState();
} finally {
lock.unlock();
}
}
/** Returns the hash of the last seen best-chain block, or null if the wallet is too old to store this data. */
@Nullable
public Sha256Hash getLastBlockSeenHash() {
lock.lock();
try {
return lastBlockSeenHash;
} finally {
lock.unlock();
}
}
public void setLastBlockSeenHash(@Nullable Sha256Hash lastBlockSeenHash) {
lock.lock();
try {
this.lastBlockSeenHash = lastBlockSeenHash;
} finally {
lock.unlock();
}
walletSaveLater();
}
public void setLastBlockSeenHeight(int lastBlockSeenHeight) {
lock.lock();
try {
this.lastBlockSeenHeight = lastBlockSeenHeight;
} finally {
lock.unlock();
}
walletSaveLater();
}
public void setLastBlockSeenTimeSecs(long timeSecs) {
lock.lock();
try {
lastBlockSeenTimeSecs = timeSecs;
} finally {
lock.unlock();
}
walletSaveLater();
}
/**
* Returns the UNIX time in seconds since the epoch extracted from the last best seen block header. This timestamp
* is <b>not</b> the local time at which the block was first observed by this application but rather what the block
* (i.e. miner) self declares. It is allowed to have some significant drift from the real time at which the block
* was found, although most miners do use accurate times. If this wallet is old and does not have a recorded
* time then this method returns zero.
*/
public long getLastBlockSeenTimeSecs() {
lock.lock();
try {
return lastBlockSeenTimeSecs;
} finally {
lock.unlock();
}
}
/**
* Returns a {@link java.util.Date} representing the time extracted from the last best seen block header. This timestamp
* is <b>not</b> the local time at which the block was first observed by this application but rather what the block
* (i.e. miner) self declares. It is allowed to have some significant drift from the real time at which the block
* was found, although most miners do use accurate times. If this wallet is old and does not have a recorded
* time then this method returns null.
*/
@Nullable
public Date getLastBlockSeenTime() {
final long secs = getLastBlockSeenTimeSecs();
if (secs == 0)
return null;
else
return new Date(secs * 1000);
}
/**
* Returns the height of the last seen best-chain block. Can be 0 if a wallet is brand new or -1 if the wallet
* is old and doesn't have that data.
*/
public int getLastBlockSeenHeight() {
lock.lock();
try {
return lastBlockSeenHeight;
} finally {
lock.unlock();
}
}
@Override
public Value getBalance() {
lock.lock();
try {
return getTxBalance(Iterables.concat(unspent.values(), pending.values()), true);
} finally {
lock.unlock();
}
}
Value getTxBalance(Iterable<Transaction> txs, boolean toMe) {
lock.lock();
try {
Value value = coinType.value(0);
for (Transaction tx : txs) {
if (toMe) {
value = value.add(tx.getValueSentToMe(this, false));
} else {
value = value.add(tx.getValue(this));
}
}
return value;
} finally {
lock.unlock();
}
}
/**
* Sets that the specified status is currently updating i.e. getting transactions.
*
* Returns true if registered successfully or false if status already updating
*/
@VisibleForTesting boolean registerStatusForUpdate(AddressStatus status) {
checkNotNull(status.getStatus());
lock.lock();
try {
// If current address is updating
if (statusPendingUpdates.containsKey(status.getAddress())) {
AddressStatus updatingStatus = statusPendingUpdates.get(status.getAddress());
// If the same status is updating, don't update again
if (updatingStatus.getStatus().equals(status.getStatus())) {
return false;
} else { // Status is newer, so replace the updating status
statusPendingUpdates.put(status.getAddress(), status);
return true;
}
} else { // This status is new
statusPendingUpdates.put(status.getAddress(), status);
return true;
}
}
finally {
lock.unlock();
}
}
void commitAddressStatus(AddressStatus newStatus) {
lock.lock();
try {
AddressStatus updatingStatus = statusPendingUpdates.get(newStatus.getAddress());
if (updatingStatus != null && updatingStatus.equals(newStatus)) {
statusPendingUpdates.remove(newStatus.getAddress());
}
addressesStatus.put(newStatus.getAddress(), newStatus.getStatus());
}
finally {
lock.unlock();
}
// Skip saving null statuses
if (newStatus.getStatus() != null) {
walletSaveLater();
}
}
private boolean isAddressStatusChanged(AddressStatus addressStatus) {
lock.lock();
try {
Address address = addressStatus.getAddress();
String newStatus = addressStatus.getStatus();
if (addressesStatus.containsKey(address)) {
String previousStatus = addressesStatus.get(address);
if (previousStatus == null) {
return newStatus != null; // Status changed if newStatus is not null
} else {
return !previousStatus.equals(newStatus);
}
}
else {
// Unused address, just mark it that we watch it
if (newStatus == null) {
commitAddressStatus(addressStatus);
return false;
}
else {
return true;
}
}
}
finally {
lock.unlock();
}
}
@Nullable
public AddressStatus getAddressStatus(Address address) {
lock.lock();
try {
if (addressesStatus.containsKey(address)) {
return new AddressStatus(address, addressesStatus.get(address));
}
else {
return null;
}
}
finally {
lock.unlock();
}
}
public List<AddressStatus> getAllAddressStatus() {
lock.lock();
try {
List<AddressStatus> statuses = new ArrayList<AddressStatus>(addressesStatus.size());
for (Map.Entry<Address, String> status : addressesStatus.entrySet()) {
statuses.add(new AddressStatus(status.getKey(), status.getValue()));
}
return statuses;
}
finally {
lock.unlock();
}
}
/**
* Returns all the addresses that are not currently watched
*/
@VisibleForTesting List<Address> getAddressesToWatch() {
ImmutableList.Builder<Address> addressesToWatch = ImmutableList.builder();
for (Address address : getActiveAddresses()) {
// If address not already subscribed or pending subscription
if (!addressesSubscribed.contains(address) && !addressesPendingSubscription.contains(address)) {
addressesToWatch.add(address);
}
}
return addressesToWatch.build();
}
private void confirmAddressSubscription(Address address) {
lock.lock();
try {
if (addressesPendingSubscription.contains(address)) {
log.debug("Subscribed to {}", address);
addressesPendingSubscription.remove(address);
addressesSubscribed.add(address);
}
}
finally {
lock.unlock();
}
}
@Override
public void onNewBlock(BlockHeader header) {
log.info("Got a {} block: {}", coinType.getName(), header.getBlockHeight());
boolean shouldSave = false;
lock.lock();
try {
lastBlockSeenTimeSecs = header.getTimestamp();
lastBlockSeenHeight = header.getBlockHeight();
for (Transaction tx : getTransactions(false)) {
TransactionConfidence confidence = tx.getConfidence();
// Save wallet when we have new TXs
if (confidence.getDepthInBlocks() < TX_DEPTH_SAVE_THRESHOLD) shouldSave = true;
maybeUpdateBlockDepth(confidence);
}
queueOnNewBlock();
} finally {
lock.unlock();
}
if (shouldSave) walletSaveLater();
}
private void maybeUpdateBlockDepth(TransactionConfidence confidence) {
if (confidence.getConfidenceType() != TransactionConfidence.ConfidenceType.BUILDING) return;
int newDepth = lastBlockSeenHeight - confidence.getAppearedAtChainHeight() + 1;
if (newDepth > 1) confidence.setDepthInBlocks(newDepth);
}
@Override
public void onAddressStatusUpdate(AddressStatus status) {
log.debug("Got a status {}", status);
lock.lock();
try {
confirmAddressSubscription(status.getAddress());
if (status.getStatus() != null) {
markAddressAsUsed(status.getAddress());
subscribeIfNeeded();
if (isAddressStatusChanged(status)) {
// Status changed, time to update
if (registerStatusForUpdate(status)) {
log.info("Must get transactions for address {}, status {}",
status.getAddress(), status.getStatus());
if (blockchainConnection != null) {
blockchainConnection.getHistoryTx(status, this);
}
} else {
log.info("Status {} already updating", status.getStatus());
}
}
}
else {
// Address not used, just update the status
commitAddressStatus(status);
}
}
finally {
lock.unlock();
}
}
@Override
public void onTransactionHistory(AddressStatus status, List<ServerClient.HistoryTx> historyTxes) {
lock.lock();
try {
AddressStatus updatingStatus = statusPendingUpdates.get(status.getAddress());
// Check if this updating status is valid
if (updatingStatus != null && updatingStatus.equals(status)) {
updatingStatus.queueHistoryTransactions(historyTxes);
fetchTransactions(historyTxes);
tryToApplyState(updatingStatus);
} else {
log.info("Ignoring history tx call because no entry found or newer entry.");
}
}
finally {
lock.unlock();
}
}
/**
* Try to apply all address states
*/
private void tryToApplyState() {
lock.lock();
try {
// Make a copy of statusPendingUpdates.values() because we modify it later
for (AddressStatus status : Lists.newArrayList(statusPendingUpdates.values())) {
tryToApplyState(status);
}
} finally {
lock.unlock();
}
}
/**
* Try to apply the status state
*/
private void tryToApplyState(AddressStatus status) {
lock.lock();
try {
if (statusPendingUpdates.containsKey(status.getAddress()) && status.isReady()) {
HashSet<Sha256Hash> txHashes = status.getAllTransactionHashes();
HashMap<Sha256Hash, Transaction> txs = getTransactions(txHashes);
// We have all the transactions, apply state
if (txs.size() == txHashes.size()) {
applyState(status, txs);
}
}
} finally {
lock.unlock();
}
}
private void applyState(AddressStatus status, HashMap<Sha256Hash, Transaction> txs) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
log.info("Applying state {} - {}", status.getAddress(), status.getStatus());
// Connect inputs to outputs
for (ServerClient.HistoryTx historyTx : status.getHistoryTxs()) {
Transaction tx = txs.get(historyTx.getTxHash());
if (tx != null) {
log.info("{} getHeight() = " + historyTx.getHeight(), historyTx.getTxHash());
if (historyTx.getHeight() > 0 && tx.getConfidence().getDepthInBlocks() == 0) {
TransactionConfidence confidence = tx.getConfidence();
confidence.setAppearedAtChainHeight(historyTx.getHeight());
maybeUpdateBlockDepth(confidence);
}
} else {
log.error("Could not find {} in the transactions pool. Aborting applying state",
historyTx.getTxHash());
return;
}
}
for (Transaction tx : txs.values()) {
connectTransaction(tx);
}
commitAddressStatus(status);
queueOnNewBalance();
}
private void connectTransaction(Transaction tx) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
// Connect to other transactions in the wallet pocket
if (log.isInfoEnabled()) log.info("Connecting inputs of tx {}", tx.getHash());
int txiIndex = 0;
for (TransactionInput txi : tx.getInputs()) {
TransactionOutput output = txi.getConnectedOutput();
if (output != null && !output.isAvailableForSpending()) {
// Check if the current input spends this output
if (output.getSpentBy() == null || output.getSpentBy().equals(txi)) {
log.info("skipping an already connected txi {}", txi);
txiIndex++;
continue; // skip connected inputs
}
}
Sha256Hash outputHash = txi.getOutpoint().getHash();
Transaction fromTx = transactions.get(outputHash);
if (fromTx != null) {
// Try to connect and recover if failed once.
for (int i = 2; i > 0; i--) {
TransactionInput.ConnectionResult result = txi.connect(fromTx, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
log.error("Could not connect {} to {}", txi.getOutpoint(), fromTx.getHash());
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
TransactionOutput out = fromTx.getOutput((int) txi.getOutpoint().getIndex());
log.warn("Already spent {}, forcing unspent and retry", out);
out.markAsUnspent();
} else {
if (log.isInfoEnabled()) {
log.info("Connected {}:{} to {}:{}", fromTx.getHash(),
txi.getOutpoint().getIndex(), tx.getHashAsString(), txiIndex);
}
break; // No errors, break the loop
}
}
// Could become spent, maybe change pool
maybeMovePool(fromTx);
}
else {
log.info("No output found for input {}:{}", tx.getHashAsString(),
txiIndex);
}
txiIndex++;
}
maybeMovePool(tx);
}
/**
* If the transactions outputs are all marked as spent, and it's in the unspent map, move it.
* If the owned transactions outputs are not all marked as spent, and it's in the spent map, move it.
*/
private void maybeMovePool(Transaction tx) {
lock.lock();
try {
log.info("maybeMovePool {} tx {} {}", tx.isEveryOwnedOutputSpent(this) ? WalletTransaction.Pool.SPENT : WalletTransaction.Pool.UNSPENT,
tx.getHash(), tx.getConfidence().getConfidenceType());
if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) {
// Transaction is confirmed, move it
if (pending.remove(tx.getHash()) != null) {
if (tx.isEveryOwnedOutputSpent(this)) {
if (log.isInfoEnabled()) log.info(" {} <-pending ->spent", tx.getHash());
spent.put(tx.getHash(), tx);
} else {
if (log.isInfoEnabled()) log.info(" {} <-pending ->unspent", tx.getHash());
unspent.put(tx.getHash(), tx);
}
} else {
maybeFlipSpentUnspent(tx);
}
}
} finally {
lock.unlock();
}
}
/**
* Will flip transaction from spent/unspent pool if needed.
*/
private void maybeFlipSpentUnspent(Transaction tx) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
if (tx.isEveryOwnedOutputSpent(this)) {
// There's nothing left I can spend in this transaction.
if (unspent.remove(tx.getHash()) != null) {
if (log.isInfoEnabled()) log.info(" {} <-unspent ->spent", tx.getHash());
spent.put(tx.getHash(), tx);
}
} else {
if (spent.remove(tx.getHash()) != null) {
if (log.isInfoEnabled()) log.info(" {} <-spent ->unspent", tx.getHash());
unspent.put(tx.getHash(), tx);
}
}
}
private void fetchTransactions(List<? extends ServerClient.HistoryTx> txes) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
for (ServerClient.HistoryTx tx : txes) {
fetchTransactionIfNeeded(tx.getTxHash());
}
}
private void fetchTransactionIfNeeded(Sha256Hash txHash) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
// Check if need to fetch the transaction
if (!isTransactionAvailableOrQueued(txHash)) {
log.info("Going to fetch transaction with hash {}", txHash);
fetchingTransactions.add(txHash);
if (blockchainConnection != null) {
blockchainConnection.getTransaction(txHash, this);
}
}
}
private boolean isTransactionAvailableOrQueued(Sha256Hash txHash) {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
return getTransaction(txHash) != null || fetchingTransactions.contains(txHash);
}
@VisibleForTesting
void addNewTransactionIfNeeded(Transaction tx) {
lock.lock();
try {
// If was fetching this tx, remove it
fetchingTransactions.remove(tx.getHash());
// This tx not in wallet, add it
if (getTransaction(tx.getHash()) == null) {
tx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.PENDING);
addWalletTransaction(WalletTransaction.Pool.PENDING, tx, true);
}
} finally {
lock.unlock();
}
}
@Override
public void onTransactionUpdate(Transaction tx) {
if (log.isInfoEnabled()) log.info("Got a new transaction {}", tx.getHash());
lock.lock();
try {
addNewTransactionIfNeeded(tx);
tryToApplyState();
}
finally {
lock.unlock();
}
}
@Override
public void onTransactionBroadcast(Transaction tx) {
lock.lock();
try {
log.info("Transaction sent {}", tx);
//FIXME, when enabled it breaks the transactions connections and we get an incorrect coin balance
addNewTransactionIfNeeded(tx);
} finally {
lock.unlock();
}
queueOnTransactionBroadcastSuccess(tx);
}
@Override
public void onTransactionBroadcastError(Transaction tx) {
queueOnTransactionBroadcastFailure(tx);
}
@Override
public void onConnection(BlockchainConnection blockchainConnection) {
this.blockchainConnection = blockchainConnection;
clearTransientState();
subscribeToBlockchain();
subscribeIfNeeded();
queueOnConnectivity();
}
@Override
public void onDisconnect() {
blockchainConnection = null;
clearTransientState();
queueOnConnectivity();
}
private void subscribeToBlockchain() {
lock.lock();
try {
if (blockchainConnection != null) {
blockchainConnection.subscribeToBlockchain(this);
}
} finally {
lock.unlock();
}
}
void subscribeIfNeeded() {
lock.lock();
try {
if (blockchainConnection != null) {
List<Address> addressesToWatch = getAddressesToWatch();
if (!addressesToWatch.isEmpty()) {
addressesPendingSubscription.addAll(addressesToWatch);
blockchainConnection.subscribeToAddresses(addressesToWatch, this);
}
}
} catch (Exception e) {
log.error("Error subscribing to addresses", e);
} finally {
lock.unlock();
}
}
private void clearTransientState() {
addressesSubscribed.clear();
addressesPendingSubscription.clear();
statusPendingUpdates.clear();
fetchingTransactions.clear();
}
public void restoreWalletTransactions(ArrayList<WalletTransaction> wtxs) {
// FIXME There is a very rare bug that doesn't properly persist tx connections, need to do a sanity check by reconnecting transactions
lock.lock();
try {
for (WalletTransaction wtx : wtxs) {
simpleAddTransaction(wtx.getPool(), wtx.getTransaction());
markNotOwnOutputs(wtx.getTransaction());
}
for (Transaction utx : getTransactions(false)) {
connectTransaction(utx);
}
} finally {
lock.unlock();
}
}
@Override
public Map<Sha256Hash, Transaction> getTransactionPool(WalletTransaction.Pool pool) {
lock.lock();
try {
switch (pool) {
case UNSPENT:
return unspent;
case SPENT:
return spent;
case PENDING:
return pending;
case DEAD:
return dead;
default:
throw new RuntimeException("Unknown wallet transaction type " + pool);
}
} finally {
lock.unlock();
}
}
void queueOnNewBalance() {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
final Value balance = getBalance();
for (final ListenerRegistration<WalletAccountEventListener> registration : listeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onNewBalance(balance);
registration.listener.onWalletChanged(TransactionWatcherWallet.this);
}
});
}
}
void queueOnNewBlock() {
checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread");
for (final ListenerRegistration<WalletAccountEventListener> registration : listeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onNewBlock(TransactionWatcherWallet.this);
registration.listener.onWalletChanged(TransactionWatcherWallet.this);
}
});
}
}
void queueOnConnectivity() {
final WalletPocketConnectivity connectivity = getConnectivityStatus();
for (final ListenerRegistration<WalletAccountEventListener> registration : listeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onConnectivityStatus(connectivity);
registration.listener.onWalletChanged(TransactionWatcherWallet.this);
}
});
}
}
void queueOnTransactionBroadcastSuccess(final Transaction tx) {
for (final ListenerRegistration<WalletAccountEventListener> registration : listeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onTransactionBroadcastSuccess(TransactionWatcherWallet.this, tx);
}
});
}
}
void queueOnTransactionBroadcastFailure(final Transaction tx) {
for (final ListenerRegistration<WalletAccountEventListener> registration : listeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onTransactionBroadcastFailure(TransactionWatcherWallet.this, tx);
}
});
}
}
public void addEventListener(WalletAccountEventListener listener) {
addEventListener(listener, Threading.USER_THREAD);
}
public void addEventListener(WalletAccountEventListener listener, Executor executor) {
listeners.add(new ListenerRegistration<>(listener, executor));
}
public boolean removeEventListener(WalletAccountEventListener listener) {
return ListenerRegistration.removeFromList(listener, listeners);
}
public boolean isLoading() {
return !addressesPendingSubscription.isEmpty() || !statusPendingUpdates.isEmpty() || !fetchingTransactions.isEmpty();
}
public boolean broadcastTxSync(Transaction tx) throws IOException {
if (isConnected()) {
if (log.isInfoEnabled()) {
log.info("Broadcasting tx {}", Utils.HEX.encode(tx.bitcoinSerialize()));
}
boolean success = blockchainConnection.broadcastTxSync(tx);
if (success) {
onTransactionBroadcast(tx);
} else {
onTransactionBroadcastError(tx);
}
return success;
} else {
throw new IOException("No connection available");
}
}
public void broadcastTx(Transaction tx) throws IOException {
broadcastTx(tx, this);
}
private void broadcastTx(Transaction tx, TransactionEventListener listener) throws IOException {
if (isConnected()) {
if (log.isInfoEnabled()) {
log.info("Broadcasting tx {}", Utils.HEX.encode(tx.bitcoinSerialize()));
}
blockchainConnection.broadcastTx(tx, listener != null ? listener : this);
} else {
throw new IOException("No connection available");
}
}
public boolean isConnected() {
return blockchainConnection != null;
}
public WalletPocketConnectivity getConnectivityStatus() {
if (!isConnected()) {
return WalletPocketConnectivity.DISCONNECTED;
} else {
if (isLoading()) {
// TODO support LOADING state, for now is just CONNECTED
return WalletPocketConnectivity.CONNECTED;
} else {
return WalletPocketConnectivity.CONNECTED;
}
}
}
@Override
public Map<Sha256Hash, Transaction> getUnspentTransactions() {
return unspent;
}
@Override
public Map<Sha256Hash, Transaction> getPendingTransactions() {
return pending;
}
@Override
public Map<Sha256Hash, Transaction> getTransactions() {
return transactions;
}
}