/*
* 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);
}
}
}
}