io.bitsquare.btc.WalletService.java Source code

Java tutorial

Introduction

Here is the source code for io.bitsquare.btc.WalletService.java

Source

/*
 * This file is part of Bitsquare.
 *
 * Bitsquare is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
 */

package io.bitsquare.btc;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.Service;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import io.bitsquare.app.Log;
import io.bitsquare.btc.listeners.AddressConfidenceListener;
import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.btc.listeners.TxConfidenceListener;
import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ExceptionHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.network.DnsLookupTor;
import io.bitsquare.network.NetworkOptionKeys;
import io.bitsquare.network.Socks5MultiDiscovery;
import io.bitsquare.network.Socks5ProxyProvider;
import io.bitsquare.storage.FileUtil;
import io.bitsquare.storage.Storage;
import io.bitsquare.user.Preferences;
import javafx.beans.property.*;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.DeterministicSeed;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * WalletService handles all non trade specific wallet and bitcoin related services.
 * It startup the wallet app kit and initialized the wallet.
 */
public class WalletService {
    private static final Logger log = LoggerFactory.getLogger(WalletService.class);

    private static final long STARTUP_TIMEOUT_SEC = 60;

    private final CopyOnWriteArraySet<AddressConfidenceListener> addressConfidenceListeners = new CopyOnWriteArraySet<>();
    private final CopyOnWriteArraySet<TxConfidenceListener> txConfidenceListeners = new CopyOnWriteArraySet<>();
    private final CopyOnWriteArraySet<BalanceListener> balanceListeners = new CopyOnWriteArraySet<>();

    private final DownloadListener downloadListener = new DownloadListener();
    private final WalletEventListener walletEventListener = new BitsquareWalletEventListener();

    private final RegTestHost regTestHost;
    private final TradeWalletService tradeWalletService;
    private final AddressEntryList addressEntryList;
    private final Preferences preferences;
    private final Socks5ProxyProvider socks5ProxyProvider;
    private final NetworkParameters params;
    private final File walletDir;
    private final UserAgent userAgent;
    private final int socks5DiscoverMode;

    private WalletAppKitBitSquare walletAppKit;
    private Wallet wallet;
    private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
    private final ObjectProperty<List<Peer>> connectedPeers = new SimpleObjectProperty<>();
    public final BooleanProperty shutDownDone = new SimpleBooleanProperty();
    private final Storage<Long> storage;
    private final Long bloomFilterTweak;
    private KeyParameter aesKey;

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Constructor
    ///////////////////////////////////////////////////////////////////////////////////////////

    @Inject
    public WalletService(RegTestHost regTestHost, TradeWalletService tradeWalletService,
            AddressEntryList addressEntryList, UserAgent userAgent, Preferences preferences,
            Socks5ProxyProvider socks5ProxyProvider, @Named(BtcOptionKeys.WALLET_DIR) File appDir,
            @Named(NetworkOptionKeys.SOCKS5_DISCOVER_MODE) String socks5DiscoverModeString) {
        this.regTestHost = regTestHost;
        this.tradeWalletService = tradeWalletService;
        this.addressEntryList = addressEntryList;
        this.preferences = preferences;
        this.socks5ProxyProvider = socks5ProxyProvider;
        this.params = preferences.getBitcoinNetwork().getParameters();
        this.walletDir = new File(appDir, "bitcoin");
        this.userAgent = userAgent;

        storage = new Storage<>(walletDir);
        Long persisted = storage.initAndGetPersistedWithFileName("BloomFilterNonce");
        if (persisted != null) {
            bloomFilterTweak = persisted;
        } else {
            bloomFilterTweak = new Random().nextLong();
            storage.queueUpForSave(bloomFilterTweak, 100);
        }

        String[] socks5DiscoverModes = StringUtils.deleteWhitespace(socks5DiscoverModeString).split(",");
        int mode = 0;
        for (int i = 0; i < socks5DiscoverModes.length; i++) {
            switch (socks5DiscoverModes[i]) {
            case "ADDR":
                mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ADDR;
                break;
            case "DNS":
                mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_DNS;
                break;
            case "ONION":
                mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ONION;
                break;
            case "ALL":
            default:
                mode |= Socks5MultiDiscovery.SOCKS5_DISCOVER_ALL;
                break;
            }
        }
        socks5DiscoverMode = mode;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Public Methods
    ///////////////////////////////////////////////////////////////////////////////////////////

    public void initialize(@Nullable DeterministicSeed seed, ResultHandler resultHandler,
            ExceptionHandler exceptionHandler) {
        Log.traceCall();
        // Tell bitcoinj to execute event handlers on the JavaFX UI thread. This keeps things simple and means
        // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener
        // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in
        // a future version.

        Threading.USER_THREAD = UserThread.getExecutor();

        Timer timeoutTimer = UserThread.runAfter(
                () -> exceptionHandler.handleException(
                        new TimeoutException("Wallet did not initialize in " + STARTUP_TIMEOUT_SEC + " seconds.")),
                STARTUP_TIMEOUT_SEC);

        backupWallet();

        final Socks5Proxy socks5Proxy = preferences.getUseTorForBitcoinJ() ? socks5ProxyProvider.getSocks5Proxy()
                : null;
        log.debug("Use socks5Proxy for bitcoinj: " + socks5Proxy);

        // If seed is non-null it means we are restoring from backup.
        walletAppKit = new WalletAppKitBitSquare(params, socks5Proxy, walletDir, "Bitsquare") {
            @Override
            protected void onSetupCompleted() {
                // Don't make the user wait for confirmations for now, as the intention is they're sending it
                // their own money!
                walletAppKit.wallet().allowSpendingUnconfirmedTransactions();
                final PeerGroup peerGroup = walletAppKit.peerGroup();

                if (params != RegTestParams.get())
                    peerGroup.setMaxConnections(11);

                // We don't want to get our node white list polluted with nodes from AddressMessage calls.
                if (preferences.getBitcoinNodes() != null && !preferences.getBitcoinNodes().isEmpty())
                    peerGroup.setAddPeersFromAddressMessage(false);

                wallet = walletAppKit.wallet();
                wallet.addEventListener(walletEventListener);

                addressEntryList.onWalletReady(wallet);

                peerGroup.addEventListener(new PeerEventListener() {
                    @Override
                    public void onPeersDiscovered(Set<PeerAddress> peerAddresses) {
                    }

                    @Override
                    public void onBlocksDownloaded(Peer peer, Block block, FilteredBlock filteredBlock,
                            int blocksLeft) {
                    }

                    @Override
                    public void onChainDownloadStarted(Peer peer, int blocksLeft) {
                    }

                    @Override
                    public void onPeerConnected(Peer peer, int peerCount) {
                        numPeers.set(peerCount);
                        connectedPeers.set(peerGroup.getConnectedPeers());
                    }

                    @Override
                    public void onPeerDisconnected(Peer peer, int peerCount) {
                        numPeers.set(peerCount);
                        connectedPeers.set(peerGroup.getConnectedPeers());
                    }

                    @Override
                    public Message onPreMessageReceived(Peer peer, Message m) {
                        return null;
                    }

                    @Override
                    public void onTransaction(Peer peer, Transaction t) {
                    }

                    @Nullable
                    @Override
                    public List<Message> getData(Peer peer, GetDataMessage m) {
                        return null;
                    }
                });

                // set after wallet is ready
                tradeWalletService.setWalletAppKit(walletAppKit);
                tradeWalletService.setAddressEntryList(addressEntryList);
                timeoutTimer.stop();

                // onSetupCompleted in walletAppKit is not the called on the last invocations, so we add a bit of delay
                UserThread.runAfter(resultHandler::handleResult, 100, TimeUnit.MILLISECONDS);
            }
        };

        // Bloom filters in BitcoinJ are completely broken
        // See: https://jonasnick.github.io/blog/2015/02/12/privacy-in-bitcoinj/
        // Here are a few improvements to fix a few vulnerabilities.

        // Bitsquare's BitcoinJ fork has added a bloomFilterTweak (nonce) setter to reuse the same seed avoiding the trivial vulnerability
        // by getting the real pub keys by intersections of several filters sent at each startup.
        walletAppKit.setBloomFilterTweak(bloomFilterTweak);

        // Avoid the simple attack (see: https://jonasnick.github.io/blog/2015/02/12/privacy-in-bitcoinj/) due to the 
        // default implementation using both pubkey and hash of pubkey. We have set a insertPubKey flag in BasicKeyChain to default false.

        // Default only 266 keys are generated (2 * 100+33). That would trigger new bloom filters when we are reaching 
        // the threshold. To avoid reaching the threshold we create much more keys which are unlikely to cause update of the
        // filter for most users. With lookaheadSize of 500 we get 1333 keys which should be enough for most users to 
        // never need to update a bloom filter, which would weaken privacy.
        walletAppKit.setLookaheadSize(500);

        // Calculation is derived from: https://www.reddit.com/r/Bitcoin/comments/2vrx6n/privacy_in_bitcoinj_android_wallet_multibit_hive/coknjuz
        // No. of false positives (56M keys in the blockchain): 
        // First attempt for FP rate:
        // FP rate = 0,0001;  No. of false positives: 0,0001 * 56 000 000  = 5600
        // We have 1333keys: 1333 / (5600 + 1333) = 0.19 -> 19 % probability that a pub key is in our wallet
        // After tests I found out that the bandwidth consumption varies widely related to the generated filter.
        // About 20- 40 MB for upload and 30-130 MB for download at first start up (spv chain).
        // Afterwards its about 1 MB for upload and 20-80 MB for download.
        // Probably better then a high FP rate would be to include foreign pubKeyHashes which are tested to not be used 
        // in many transactions. If we had a pool of 100 000 such keys (2 MB data dump) to random select 4000 we could mix it with our
        // 1000 own keys and get a similar probability rate as with the current setup but less variation in bandwidth 
        // consumption.

        // For now to reduce risks with high bandwidth consumption we reduce the FP rate by half.
        // FP rate = 0,00005;  No. of false positives: 0,00005 * 56 000 000  = 2800
        // 1333 / (2800 + 1333) = 0.32 -> 32 % probability that a pub key is in our wallet
        walletAppKit.setBloomFilterFalsePositiveRate(0.00005);

        String btcNodes = preferences.getBitcoinNodes();
        log.debug("btcNodes: " + btcNodes);
        boolean usePeerNodes = false;

        // Pass custom seed nodes if set in options
        if (!btcNodes.isEmpty()) {
            String[] nodes = StringUtils.deleteWhitespace(btcNodes).split(",");
            List<PeerAddress> peerAddressList = new ArrayList<>();
            for (String node : nodes) {
                String[] parts = node.split(":");
                if (parts.length == 1) {
                    // port not specified.  Use default port for network.
                    parts = new String[] { parts[0], Integer.toString(params.getPort()) };
                }
                if (parts.length == 2) {
                    // note: this will cause a DNS request if hostname used.
                    // note: DNS requests are routed over socks5 proxy, if used.
                    // note: .onion hostnames will be unresolved.
                    InetSocketAddress addr;
                    if (socks5Proxy != null) {
                        try {
                            // proxy remote DNS request happens here.  blocking.
                            addr = new InetSocketAddress(DnsLookupTor.lookup(socks5Proxy, parts[0]),
                                    Integer.parseInt(parts[1]));
                        } catch (Exception e) {
                            log.warn("Dns lookup failed for host: {}", parts[0]);
                            addr = null;
                        }
                    } else {
                        // DNS request happens here. if it fails, addr.isUnresolved() == true.
                        addr = new InetSocketAddress(parts[0], Integer.parseInt(parts[1]));
                    }
                    if (addr != null && !addr.isUnresolved()) {
                        peerAddressList.add(new PeerAddress(addr.getAddress(), addr.getPort()));
                    }
                }
            }
            if (peerAddressList.size() > 0) {
                PeerAddress peerAddressListFixed[] = new PeerAddress[peerAddressList.size()];
                log.debug("btcNodes parsed: " + Arrays.toString(peerAddressListFixed));

                walletAppKit.setPeerNodes(peerAddressList.toArray(peerAddressListFixed));
                usePeerNodes = true;
            }
        }

        // Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen
        // or progress widget to keep the user engaged whilst we initialise, but we don't.
        if (params == RegTestParams.get()) {
            if (regTestHost == RegTestHost.REG_TEST_SERVER) {
                try {
                    walletAppKit.setPeerNodes(
                            new PeerAddress(InetAddress.getByName(RegTestHost.SERVER_IP), params.getPort()));
                    usePeerNodes = true;
                } catch (UnknownHostException e) {
                    throw new RuntimeException(e);
                }
            } else if (regTestHost == RegTestHost.LOCALHOST) {
                walletAppKit.connectToLocalHost(); // You should run a regtest mode bitcoind locally.}
            }
        } else if (params == MainNetParams.get()) {
            // Checkpoints are block headers that ship inside our app: for a new user, we pick the last header
            // in the checkpoints file and then download the rest from the network. It makes things much faster.
            // Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the
            // last months worth or more (takes a few seconds).
            try {
                walletAppKit.setCheckpoints(getClass().getResourceAsStream("/wallet/checkpoints"));
            } catch (Exception e) {
                e.printStackTrace();
                log.error(e.toString());
            }
        } else if (params == TestNet3Params.get()) {
            walletAppKit.setCheckpoints(getClass().getResourceAsStream("/wallet/checkpoints.testnet"));
        }

        // If operating over a proxy and we haven't set any peer nodes, then
        // we want to use SeedPeers for discovery instead of the default DnsDiscovery.
        // This is only because we do not yet have a Dns discovery class that works
        // reliably over proxy/tor.
        //
        // todo: There should be a user pref called "Use Local DNS for Proxy/Tor"
        // that disables this.  In that case, the default DnsDiscovery class will
        // be used which should work, but is less private.  The aim here is to
        // be private by default when using proxy/tor.  However, the seedpeers
        // could become outdated, so it is important that the user be able to
        // disable it, but should be made aware of the reduced privacy.
        if (socks5Proxy != null && !usePeerNodes) {
            // SeedPeers uses hard coded stable addresses (from MainNetParams). It should be updated from time to time.
            walletAppKit.setDiscovery(new Socks5MultiDiscovery(socks5Proxy, params, socks5DiscoverMode));
        }

        walletAppKit.setDownloadListener(downloadListener).setBlockingStartup(false)
                .setUserAgent(userAgent.getName(), userAgent.getVersion()).restoreWalletFromSeed(seed);

        walletAppKit.addListener(new Service.Listener() {
            @Override
            public void failed(@NotNull Service.State from, @NotNull Throwable failure) {
                walletAppKit = null;
                log.error("walletAppKit failed");
                timeoutTimer.stop();
                UserThread.execute(() -> exceptionHandler.handleException(failure));
            }
        }, Threading.USER_THREAD);
        walletAppKit.startAsync();
    }

    public void shutDown() {
        if (wallet != null)
            wallet.removeEventListener(walletEventListener);

        if (walletAppKit != null) {
            try {
                walletAppKit.stopAsync();
                walletAppKit.awaitTerminated(5, TimeUnit.SECONDS);
            } catch (Throwable e) {
                // ignore
            }
            shutDownDone.set(true);
        }
    }

    public String exportWalletData(boolean includePrivKeys) {
        StringBuilder addressEntryListData = new StringBuilder();
        getAddressEntryListAsImmutableList().stream()
                .forEach(e -> addressEntryListData.append(e.toString()).append("\n"));
        return "BitcoinJ wallet:\n" + wallet.toString(includePrivKeys, true, true, walletAppKit.chain()) + "\n\n"
                + "Bitsquare address entry list:\n" + addressEntryListData.toString() + "All pubkeys as hex:\n"
                + wallet.printAllPubKeysAsHex();
    }

    public void restoreSeedWords(DeterministicSeed seed, ResultHandler resultHandler,
            ExceptionHandler exceptionHandler) {
        Context ctx = Context.get();
        new Thread(() -> {
            try {
                Context.propagate(ctx);
                walletAppKit.stopAsync();
                walletAppKit.awaitTerminated();
                initialize(seed, resultHandler, exceptionHandler);
            } catch (Throwable t) {
                t.printStackTrace();
                log.error("Executing task failed. " + t.getMessage());
            }
        }, "RestoreWallet-%d").start();
    }

    public void backupWallet() {
        FileUtil.rollingBackup(walletDir, "Bitsquare.wallet", 20);
    }

    public void clearBackup() {
        try {
            FileUtil.deleteDirectory(new File(Paths.get(walletDir.getAbsolutePath(), "backup").toString()));
        } catch (IOException e) {
            log.error("Could not delete directory " + e.getMessage());
            e.printStackTrace();
        }
    }

    public void setAesKey(KeyParameter aesKey) {
        this.aesKey = aesKey;
    }

    public void decryptWallet(@NotNull KeyParameter key) {
        wallet.decrypt(key);
        addressEntryList.stream().forEach(e -> {
            final DeterministicKey keyPair = e.getKeyPair();
            if (keyPair != null && keyPair.isEncrypted())
                e.setDeterministicKey(keyPair.decrypt(key));
        });

        setAesKey(null);
        addressEntryList.queueUpForSave();
    }

    public void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) {
        if (this.aesKey != null) {
            log.warn("encryptWallet called but we have a aesKey already set. "
                    + "We decryptWallet with the old key before we apply the new key.");
            decryptWallet(this.aesKey);
        }

        wallet.encrypt(keyCrypterScrypt, key);
        addressEntryList.stream().forEach(e -> {
            final DeterministicKey keyPair = e.getKeyPair();
            if (keyPair != null && keyPair.isEncrypted())
                e.setDeterministicKey(keyPair.encrypt(keyCrypterScrypt, key));
        });
        setAesKey(key);
        addressEntryList.queueUpForSave();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Listener
    ///////////////////////////////////////////////////////////////////////////////////////////

    public void addAddressConfidenceListener(AddressConfidenceListener listener) {
        addressConfidenceListeners.add(listener);
    }

    public void removeAddressConfidenceListener(AddressConfidenceListener listener) {
        addressConfidenceListeners.remove(listener);
    }

    public void addTxConfidenceListener(TxConfidenceListener listener) {
        txConfidenceListeners.add(listener);
    }

    public void removeTxConfidenceListener(TxConfidenceListener listener) {
        txConfidenceListeners.remove(listener);
    }

    public void addBalanceListener(BalanceListener listener) {
        balanceListeners.add(listener);
    }

    public void removeBalanceListener(BalanceListener listener) {
        balanceListeners.remove(listener);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // AddressEntry
    ///////////////////////////////////////////////////////////////////////////////////////////

    public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) {
        Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
                .filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
        if (addressEntry.isPresent()) {
            return addressEntry.get();
        } else {
            AddressEntry entry = addressEntryList.addAddressEntry(
                    new AddressEntry(wallet.freshReceiveKey(), wallet.getParams(), context, offerId));
            saveAddressEntryList();
            return entry;
        }
    }

    public AddressEntry getOrCreateAddressEntry(AddressEntry.Context context) {
        Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
                .filter(e -> context == e.getContext()).findAny();
        return getOrCreateAddressEntry(context, addressEntry);
    }

    public AddressEntry getOrCreateUnusedAddressEntry(AddressEntry.Context context) {
        Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
                .filter(e -> context == e.getContext()).filter(e -> getNumTxOutputsForAddress(e.getAddress()) == 0)
                .findAny();
        return getOrCreateAddressEntry(context, addressEntry);
    }

    private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context,
            Optional<AddressEntry> addressEntry) {
        if (addressEntry.isPresent()) {
            return addressEntry.get();
        } else {
            AddressEntry entry = addressEntryList
                    .addAddressEntry(new AddressEntry(wallet.freshReceiveKey(), wallet.getParams(), context));
            saveAddressEntryList();
            return entry;
        }
    }

    public Optional<AddressEntry> findAddressEntry(String address, AddressEntry.Context context) {
        return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString()))
                .filter(e -> context == e.getContext()).findAny();
    }

    public List<AddressEntry> getAvailableAddressEntries() {
        return getAddressEntryListAsImmutableList().stream()
                .filter(addressEntry -> AddressEntry.Context.AVAILABLE == addressEntry.getContext())
                .collect(Collectors.toList());
    }

    public List<AddressEntry> getAddressEntries(AddressEntry.Context context) {
        return getAddressEntryListAsImmutableList().stream()
                .filter(addressEntry -> context == addressEntry.getContext()).collect(Collectors.toList());
    }

    public List<AddressEntry> getFundedAvailableAddressEntries() {
        return getAvailableAddressEntries().stream()
                .filter(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).isPositive())
                .collect(Collectors.toList());
    }

    public List<AddressEntry> getAddressEntryListAsImmutableList() {
        return ImmutableList.copyOf(addressEntryList);
    }

    public void swapTradeEntryToAvailableEntry(String offerId, AddressEntry.Context context) {
        Optional<AddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream()
                .filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
        addressEntryOptional.ifPresent(e -> {
            addressEntryList.swapToAvailable(e);
            saveAddressEntryList();
        });
    }

    public void swapAnyTradeEntryContextToAvailableEntry(String offerId) {
        swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING);
        swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE);
        swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.MULTI_SIG);
        swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.TRADE_PAYOUT);
    }

    public void saveAddressEntryList() {
        addressEntryList.queueUpForSave();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // TransactionConfidence
    ///////////////////////////////////////////////////////////////////////////////////////////

    public TransactionConfidence getConfidenceForAddress(Address address) {
        List<TransactionConfidence> transactionConfidenceList = new ArrayList<>();
        if (wallet != null) {
            Set<Transaction> transactions = wallet.getTransactions(true);
            if (transactions != null) {
                transactionConfidenceList.addAll(transactions.stream()
                        .map(tx -> getTransactionConfidence(tx, address)).collect(Collectors.toList()));
            }
        }
        return getMostRecentConfidence(transactionConfidenceList);
    }

    public TransactionConfidence getConfidenceForTxId(String txId) {
        if (wallet != null) {
            Set<Transaction> transactions = wallet.getTransactions(true);
            for (Transaction tx : transactions) {
                if (tx.getHashAsString().equals(txId))
                    return tx.getConfidence();
            }
        }
        return null;
    }

    private TransactionConfidence getTransactionConfidence(Transaction tx, Address address) {
        List<TransactionOutput> mergedOutputs = getOutputsWithConnectedOutputs(tx);
        List<TransactionConfidence> transactionConfidenceList = new ArrayList<>();

        mergedOutputs.stream()
                .filter(e -> e.getScriptPubKey().isSentToAddress() || e.getScriptPubKey().isPayToScriptHash())
                .forEach(transactionOutput -> {
                    Address outputAddress = transactionOutput.getScriptPubKey().getToAddress(params);
                    if (address.equals(outputAddress)) {
                        transactionConfidenceList.add(tx.getConfidence());
                    }
                });
        return getMostRecentConfidence(transactionConfidenceList);
    }

    private List<TransactionOutput> getOutputsWithConnectedOutputs(Transaction tx) {
        List<TransactionOutput> transactionOutputs = tx.getOutputs();
        List<TransactionOutput> connectedOutputs = new ArrayList<>();

        // add all connected outputs from any inputs as well
        List<TransactionInput> transactionInputs = tx.getInputs();
        for (TransactionInput transactionInput : transactionInputs) {
            TransactionOutput transactionOutput = transactionInput.getConnectedOutput();
            if (transactionOutput != null) {
                connectedOutputs.add(transactionOutput);
            }
        }

        List<TransactionOutput> mergedOutputs = new ArrayList<>();
        mergedOutputs.addAll(transactionOutputs);
        mergedOutputs.addAll(connectedOutputs);
        return mergedOutputs;
    }

    private TransactionConfidence getMostRecentConfidence(List<TransactionConfidence> transactionConfidenceList) {
        TransactionConfidence transactionConfidence = null;
        for (TransactionConfidence confidence : transactionConfidenceList) {
            if (confidence != null) {
                if (transactionConfidence == null
                        || confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)
                        || (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING)
                                && transactionConfidence.getConfidenceType()
                                        .equals(TransactionConfidence.ConfidenceType.BUILDING)
                                && confidence.getDepthInBlocks() < transactionConfidence.getDepthInBlocks())) {
                    transactionConfidence = confidence;
                }
            }
        }
        return transactionConfidence;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Balance
    ///////////////////////////////////////////////////////////////////////////////////////////

    // BalanceType.AVAILABLE
    public Coin getAvailableBalance() {
        return wallet != null ? wallet.getBalance(Wallet.BalanceType.AVAILABLE) : Coin.ZERO;
    }

    public Coin getBalanceForAddress(Address address) {
        return wallet != null ? getBalance(wallet.calculateAllSpendCandidates(), address) : Coin.ZERO;
    }

    private Coin getBalance(List<TransactionOutput> transactionOutputs, Address address) {
        Coin balance = Coin.ZERO;
        for (TransactionOutput transactionOutput : transactionOutputs) {
            if (transactionOutput.getScriptPubKey().isSentToAddress()
                    || transactionOutput.getScriptPubKey().isPayToScriptHash()) {
                Address addressOutput = transactionOutput.getScriptPubKey().getToAddress(params);
                if (addressOutput.equals(address))
                    balance = balance.add(transactionOutput.getValue());
            }
        }
        return balance;
    }

    public Coin getSavingWalletBalance() {
        return Coin.valueOf(getFundedAvailableAddressEntries().stream()
                .mapToLong(addressEntry -> getBalanceForAddress(addressEntry.getAddress()).value).sum());
    }

    public int getNumTxOutputsForAddress(Address address) {
        List<TransactionOutput> transactionOutputs = new ArrayList<>();
        wallet.getTransactions(true).stream().forEach(t -> transactionOutputs.addAll(t.getOutputs()));
        int outputs = 0;
        for (TransactionOutput transactionOutput : transactionOutputs) {
            if (transactionOutput.getScriptPubKey().isSentToAddress()
                    || transactionOutput.getScriptPubKey().isPayToScriptHash()) {
                Address addressOutput = transactionOutput.getScriptPubKey().getToAddress(params);
                if (addressOutput.equals(address))
                    outputs++;
            }
        }
        return outputs;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Double spend unconfirmed transaction (unlock in case we got into a tx with a too low mining fee)
    ///////////////////////////////////////////////////////////////////////////////////////////

    public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMessageHandler errorMessageHandler)
            throws InsufficientMoneyException, AddressFormatException, AddressEntryException {
        AddressEntry addressEntry = getOrCreateUnusedAddressEntry(AddressEntry.Context.AVAILABLE);
        checkNotNull(addressEntry.getAddress(), "addressEntry.getAddress() must not be null");
        Optional<Transaction> transactionOptional = wallet.getTransactions(true).stream()
                .filter(t -> t.getHashAsString().equals(txId)).findAny();
        if (transactionOptional.isPresent())
            doubleSpendTransaction(transactionOptional.get(), addressEntry.getAddress(), resultHandler,
                    errorMessageHandler);
    }

    public void doubleSpendTransaction(Transaction txToDoubleSpend, Address toAddress, Runnable resultHandler,
            ErrorMessageHandler errorMessageHandler)
            throws InsufficientMoneyException, AddressFormatException, AddressEntryException {
        final TransactionConfidence.ConfidenceType confidenceType = txToDoubleSpend.getConfidence()
                .getConfidenceType();
        if (confidenceType == TransactionConfidence.ConfidenceType.PENDING) {
            log.debug("txToDoubleSpend no. of inputs " + txToDoubleSpend.getInputs().size());

            Transaction newTransaction = new Transaction(params);
            txToDoubleSpend.getInputs().stream().forEach(input -> {
                final TransactionOutput connectedOutput = input.getConnectedOutput();
                if (connectedOutput != null && connectedOutput.isMine(wallet)
                        && connectedOutput.getParentTransaction() != null
                        && connectedOutput.getParentTransaction().getConfidence() != null
                        && input.getValue() != null) {
                    if (connectedOutput.getParentTransaction().getConfidence()
                            .getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) {
                        newTransaction.addInput(new TransactionInput(params, newTransaction, new byte[] {},
                                new TransactionOutPoint(params, input.getOutpoint().getIndex(),
                                        new Transaction(params,
                                                connectedOutput.getParentTransaction().bitcoinSerialize())),
                                Coin.valueOf(input.getValue().value)));
                    } else {
                        log.warn("Confidence of parent tx is not of type BUILDING: ConfidenceType="
                                + connectedOutput.getParentTransaction().getConfidence().getConfidenceType());
                    }
                }
            });

            log.debug("newTransaction no. of inputs " + newTransaction.getInputs().size());
            log.debug("newTransaction size in kB " + newTransaction.bitcoinSerialize().length / 1024);

            if (!newTransaction.getInputs().isEmpty()) {
                Coin amount = Coin.valueOf(newTransaction.getInputs().stream()
                        .mapToLong(input -> input.getValue() != null ? input.getValue().value : 0).sum());
                newTransaction.addOutput(amount, toAddress);

                Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(newTransaction);
                sendRequest.aesKey = aesKey;
                sendRequest.coinSelector = new TradeWalletCoinSelector(params, toAddress, false);
                sendRequest.changeAddress = toAddress;
                sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();

                Coin requiredFee = getFeeForDoubleSpend(sendRequest, toAddress, amount,
                        FeePolicy.getFixedTxFeeForTrades());

                amount = (amount.subtract(requiredFee)).subtract(FeePolicy.getFixedTxFeeForTrades());
                newTransaction.clearOutputs();
                newTransaction.addOutput(amount, toAddress);

                sendRequest = Wallet.SendRequest.forTx(newTransaction);
                sendRequest.aesKey = aesKey;
                sendRequest.coinSelector = new TradeWalletCoinSelector(params, toAddress, false);
                // We don't expect change but set it just in case
                sendRequest.changeAddress = toAddress;
                sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();

                Wallet.SendResult sendResult = null;
                try {
                    sendResult = wallet.sendCoins(sendRequest);
                } catch (InsufficientMoneyException e) {
                    // in some cases getFee did not calculate correctly and we still get an InsufficientMoneyException
                    log.warn("We still have a missing fee "
                            + (e.missing != null ? e.missing.toFriendlyString() : ""));

                    if (e != null)
                        amount = amount.subtract(e.missing);
                    newTransaction.clearOutputs();
                    newTransaction.addOutput(amount, toAddress);

                    sendRequest = Wallet.SendRequest.forTx(newTransaction);
                    sendRequest.aesKey = aesKey;
                    sendRequest.coinSelector = new TradeWalletCoinSelector(params, toAddress, false);
                    sendRequest.changeAddress = toAddress;
                    sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();

                    try {
                        sendResult = wallet.sendCoins(sendRequest);
                    } catch (InsufficientMoneyException e2) {
                        errorMessageHandler.handleErrorMessage("We did not get the correct fee calculated. "
                                + (e2.missing != null ? e2.missing.toFriendlyString() : ""));
                    }
                }
                if (sendResult != null) {
                    log.info("Broadcasting double spending transaction. " + sendResult.tx);
                    Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
                        @Override
                        public void onSuccess(Transaction result) {
                            log.info("Double spending transaction published. " + result);
                            resultHandler.run();
                        }

                        @Override
                        public void onFailure(@NotNull Throwable t) {
                            log.info("Broadcasting double spending transaction failed. " + t.getMessage());
                            errorMessageHandler.handleErrorMessage(t.getMessage());
                        }
                    });
                }
            } else {
                log.warn("sendResult is null");
                errorMessageHandler.handleErrorMessage(
                        "We could not find inputs we control in the transaction we want to double spend.");
            }
        } else if (confidenceType == TransactionConfidence.ConfidenceType.BUILDING) {
            errorMessageHandler.handleErrorMessage(
                    "That transaction is already in the blockchain so we cannot double spend it.");
        } else if (confidenceType == TransactionConfidence.ConfidenceType.DEAD) {
            errorMessageHandler
                    .handleErrorMessage("One of the inputs of that transaction has been already double spent.");
        }
    }

    private Coin getFeeForDoubleSpend(Wallet.SendRequest sendRequest, Address toAddress, Coin amount, Coin fee)
            throws AddressEntryException, AddressFormatException {
        try {
            sendRequest.tx.clearOutputs();
            sendRequest.tx.addOutput(amount, toAddress);

            Wallet.SendRequest newSendRequest = Wallet.SendRequest.forTx(sendRequest.tx);
            newSendRequest.aesKey = aesKey;
            newSendRequest.coinSelector = new TradeWalletCoinSelector(params, toAddress);
            newSendRequest.changeAddress = toAddress;
            newSendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
            wallet.completeTx(newSendRequest);

            log.debug("After fee check: amount  " + amount.toFriendlyString());
            log.debug("Output fee  " + sendRequest.tx.getFee().toFriendlyString());
            sendRequest.tx.getOutputs().stream()
                    .forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString()));
        } catch (InsufficientMoneyException e) {
            if (e.missing != null) {
                log.trace("missing fee " + e.missing.toFriendlyString());
                fee = fee.add(e.missing);
                amount = amount.subtract(fee);
                return getFeeForDoubleSpend(sendRequest, toAddress, amount, fee);
            }
        }
        return fee;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Withdrawal Fee calculation
    ///////////////////////////////////////////////////////////////////////////////////////////

    public Coin getRequiredFee(String fromAddress, String toAddress, Coin amount, AddressEntry.Context context)
            throws AddressFormatException, AddressEntryException {
        Optional<AddressEntry> addressEntry = findAddressEntry(fromAddress, context);
        if (!addressEntry.isPresent())
            throw new AddressEntryException("WithdrawFromAddress is not found in our wallet.");

        checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null");
        return getFee(fromAddress, toAddress, amount, context, Coin.ZERO);
    }

    public Coin getRequiredFeeForMultipleAddresses(Set<String> fromAddresses, String toAddress, Coin amount)
            throws AddressFormatException, AddressEntryException, InsufficientFundsException {
        Set<AddressEntry> addressEntries = fromAddresses.stream().map(address -> {
            Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
            if (!addressEntryOptional.isPresent())
                addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING);
            if (!addressEntryOptional.isPresent())
                addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT);
            if (!addressEntryOptional.isPresent())
                addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR);
            return addressEntryOptional;
        }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
        if (addressEntries.isEmpty())
            throw new AddressEntryException("No Addresses for withdraw  found in our wallet");
        return getFeeForMultipleAddresses(fromAddresses, toAddress, amount, Coin.ZERO);
    }

    private Coin getFee(String fromAddress, String toAddress, Coin amount, AddressEntry.Context context, Coin fee)
            throws AddressEntryException, AddressFormatException {
        try {
            wallet.completeTx(getSendRequest(fromAddress, toAddress, amount, aesKey, context));
        } catch (InsufficientMoneyException e) {
            if (e.missing != null) {
                log.trace("missing fee " + e.missing.toFriendlyString());
                fee = fee.add(e.missing);
                amount = amount.subtract(fee);
                return getFee(fromAddress, toAddress, amount, context, fee);
            }
        }
        log.trace("result fee " + fee.toFriendlyString());
        return fee;
    }

    private Coin getFeeForMultipleAddresses(Set<String> fromAddresses, String toAddress, Coin amount, Coin fee)
            throws AddressEntryException, AddressFormatException, InsufficientFundsException {
        try {
            wallet.completeTx(getSendRequestForMultipleAddresses(fromAddresses, toAddress, amount, null, aesKey));
        } catch (InsufficientMoneyException e) {
            if (e.missing != null) {
                log.trace("missing fee " + e.missing.toFriendlyString());
                fee = fee.add(e.missing);
                amount = amount.subtract(fee);
                if (amount.isGreaterThan(Transaction.MIN_NONDUST_OUTPUT)) {
                    return getFeeForMultipleAddresses(fromAddresses, toAddress, amount, fee);
                } else {
                    throw new InsufficientFundsException("The fees for that transaction exceed the available funds "
                            + "or the resulting output value is below the min. dust value:\n" + "Missing "
                            + e.missing.toFriendlyString());
                }
            }
        }
        log.trace("result fee " + fee.toFriendlyString());
        return fee;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Withdrawal Send
    ///////////////////////////////////////////////////////////////////////////////////////////

    public String sendFunds(String fromAddress, String toAddress, Coin receiverAmount,
            @Nullable KeyParameter aesKey, AddressEntry.Context context, FutureCallback<Transaction> callback)
            throws AddressFormatException, AddressEntryException, InsufficientMoneyException {
        Wallet.SendResult sendResult = wallet
                .sendCoins(getSendRequest(fromAddress, toAddress, receiverAmount, aesKey, context));
        Futures.addCallback(sendResult.broadcastComplete, callback);

        printTxWithInputs("sendFunds", sendResult.tx);
        return sendResult.tx.getHashAsString();
    }

    public String sendFundsForMultipleAddresses(Set<String> fromAddresses, String toAddress, Coin receiverAmount,
            @Nullable String changeAddress, @Nullable KeyParameter aesKey, FutureCallback<Transaction> callback)
            throws AddressFormatException, AddressEntryException, InsufficientMoneyException {
        Wallet.SendResult sendResult = wallet.sendCoins(getSendRequestForMultipleAddresses(fromAddresses, toAddress,
                receiverAmount, changeAddress, aesKey));
        Futures.addCallback(sendResult.broadcastComplete, callback);

        printTxWithInputs("sendFunds", sendResult.tx);
        return sendResult.tx.getHashAsString();
    }

    public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler,
            ErrorMessageHandler errorMessageHandler) throws InsufficientMoneyException, AddressFormatException {
        Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress));
        sendRequest.aesKey = aesKey;
        sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
        Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
        log.info("emptyWallet: " + sendResult.tx);
        Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
            @Override
            public void onSuccess(Transaction result) {
                log.info("onSuccess Transaction=" + result);
                resultHandler.handleResult();
            }

            @Override
            public void onFailure(@NotNull Throwable t) {
                log.error("onFailure " + t.toString());
                errorMessageHandler.handleErrorMessage(t.getMessage());
            }
        });
    }

    private Wallet.SendRequest getSendRequest(String fromAddress, String toAddress, Coin amount,
            @Nullable KeyParameter aesKey, AddressEntry.Context context)
            throws AddressFormatException, AddressEntryException, InsufficientMoneyException {
        Transaction tx = new Transaction(params);
        Preconditions.checkArgument(Restrictions.isAboveDust(amount), "The amount is too low (dust limit).");
        tx.addOutput(amount, new Address(params, toAddress));

        Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx);
        sendRequest.aesKey = aesKey;
        sendRequest.shuffleOutputs = false;
        Optional<AddressEntry> addressEntry = findAddressEntry(fromAddress, context);
        if (!addressEntry.isPresent())
            throw new AddressEntryException("WithdrawFromAddress is not found in our wallet.");

        checkNotNull(addressEntry.get(), "addressEntry.get() must not be null");
        checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must not be null");
        sendRequest.coinSelector = new TradeWalletCoinSelector(params, addressEntry.get().getAddress());
        sendRequest.changeAddress = addressEntry.get().getAddress();
        sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
        return sendRequest;
    }

    public int getTransactionSize(Set<String> fromAddresses, String toAddress, Coin amount)
            throws AddressFormatException, AddressEntryException, InsufficientMoneyException {
        Wallet.SendRequest sendRequestForMultipleAddresses = getSendRequestForMultipleAddresses(fromAddresses,
                toAddress, amount, null, aesKey);
        Transaction tx = sendRequestForMultipleAddresses.tx;
        wallet.completeTx(sendRequestForMultipleAddresses);
        log.debug("No. of inputs: " + tx.getInputs().size());
        int size = tx.bitcoinSerialize().length;
        log.debug("Tx size: " + size);
        return size;
    }

    private Wallet.SendRequest getSendRequestForMultipleAddresses(Set<String> fromAddresses, String toAddress,
            Coin amount, @Nullable String changeAddress, @Nullable KeyParameter aesKey)
            throws AddressFormatException, AddressEntryException, InsufficientMoneyException {
        Transaction tx = new Transaction(params);
        Preconditions.checkArgument(Restrictions.isAboveDust(amount), "The amount is too low (dust limit).");
        tx.addOutput(amount, new Address(params, toAddress));

        Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx);
        sendRequest.aesKey = aesKey;
        sendRequest.shuffleOutputs = false;
        Set<AddressEntry> addressEntries = fromAddresses.stream().map(address -> {
            Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
            if (!addressEntryOptional.isPresent())
                addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING);
            if (!addressEntryOptional.isPresent())
                addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT);
            if (!addressEntryOptional.isPresent())
                addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR);
            return addressEntryOptional;
        }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
        if (addressEntries.isEmpty())
            throw new AddressEntryException("No Addresses for withdraw  found in our wallet");

        sendRequest.coinSelector = new MultiAddressesCoinSelector(params, addressEntries);
        Optional<AddressEntry> addressEntryOptional = Optional.empty();
        AddressEntry changeAddressAddressEntry = null;
        if (changeAddress != null)
            addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE);

        if (addressEntryOptional.isPresent()) {
            changeAddressAddressEntry = addressEntryOptional.get();
        } else {
            ArrayList<AddressEntry> list = new ArrayList<>(addressEntries);
            if (!list.isEmpty())
                changeAddressAddressEntry = list.get(0);
        }
        checkNotNull(changeAddressAddressEntry, "change address must not be null");
        sendRequest.changeAddress = changeAddressAddressEntry.getAddress();
        sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
        return sendRequest;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Getters
    ///////////////////////////////////////////////////////////////////////////////////////////

    public ReadOnlyDoubleProperty downloadPercentageProperty() {
        return downloadListener.percentageProperty();
    }

    public Wallet getWallet() {
        return wallet;
    }

    public Transaction getTransactionFromSerializedTx(byte[] tx) {
        return new Transaction(params, tx);
    }

    public ReadOnlyIntegerProperty numPeersProperty() {
        return numPeers;
    }

    public ReadOnlyObjectProperty<List<Peer>> connectedPeersProperty() {
        return connectedPeers;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Private methods
    ///////////////////////////////////////////////////////////////////////////////////////////

    private static void printTxWithInputs(String tracePrefix, Transaction tx) {
        log.trace(tracePrefix + ": " + tx.toString());
        for (TransactionInput input : tx.getInputs()) {
            if (input.getConnectedOutput() != null)
                log.trace(
                        tracePrefix + " input value: " + input.getConnectedOutput().getValue().toFriendlyString());
            else
                log.trace(tracePrefix
                        + ": Transaction already has inputs but we don't have the connected outputs, so we don't know the value.");
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // Inner classes
    ///////////////////////////////////////////////////////////////////////////////////////////

    private static class DownloadListener extends DownloadProgressTracker {
        private final DoubleProperty percentage = new SimpleDoubleProperty(-1);

        @Override
        protected void progress(double percentage, int blocksLeft, Date date) {
            super.progress(percentage, blocksLeft, date);
            UserThread.execute(() -> this.percentage.set(percentage / 100d));
        }

        @Override
        protected void doneDownload() {
            super.doneDownload();
            UserThread.execute(() -> this.percentage.set(1d));
        }

        public ReadOnlyDoubleProperty percentageProperty() {
            return percentage;
        }
    }

    private class BitsquareWalletEventListener extends AbstractWalletEventListener {
        @Override
        public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
            notifyBalanceListeners(tx);
        }

        @Override
        public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
            notifyBalanceListeners(tx);
        }

        @Override
        public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
            for (AddressConfidenceListener addressConfidenceListener : addressConfidenceListeners) {
                List<TransactionConfidence> transactionConfidenceList = new ArrayList<>();
                transactionConfidenceList.add(getTransactionConfidence(tx, addressConfidenceListener.getAddress()));

                TransactionConfidence transactionConfidence = getMostRecentConfidence(transactionConfidenceList);
                addressConfidenceListener.onTransactionConfidenceChanged(transactionConfidence);
            }

            txConfidenceListeners.stream().filter(txConfidenceListener -> tx != null && tx.getHashAsString() != null
                    && txConfidenceListener != null && tx.getHashAsString().equals(txConfidenceListener.getTxID()))
                    .forEach(txConfidenceListener -> txConfidenceListener
                            .onTransactionConfidenceChanged(tx.getConfidence()));
        }

        private void notifyBalanceListeners(Transaction tx) {
            for (BalanceListener balanceListener : balanceListeners) {
                Coin balance;
                if (balanceListener.getAddress() != null)
                    balance = getBalanceForAddress(balanceListener.getAddress());
                else
                    balance = getAvailableBalance();

                balanceListener.onBalanceChanged(balance, tx);
            }
        }
    }
}