Keep historical bsq txos for displaying bss txs in tx list. refactor data structure. detect invalid bsq tx and show popup.

This commit is contained in:
Manfred Karrer 2017-04-03 16:02:33 -05:00
parent 0386f1e739
commit 986ac67f84
24 changed files with 544 additions and 251 deletions

View file

@ -18,11 +18,8 @@
package io.bisq.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Profiler {
private static final Logger log = LoggerFactory.getLogger(Profiler.class);
public static void printSystemLoad(Logger log) {
log.info(printSystemLoadString());
}

View file

@ -23,7 +23,6 @@ import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.script.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -41,7 +40,7 @@ class BsqCoinSelector extends BisqDefaultCoinSelector {
private final boolean permitForeignPendingTx;
private final Map<Script, Set<BsqUTXO>> utxoSetByScriptMap = new HashMap<>();
private final Map<String, Set<BsqUTXO>> utxoSetByAddressMap = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -53,11 +52,11 @@ class BsqCoinSelector extends BisqDefaultCoinSelector {
public void setUtxoMap(BsqUTXOMap bsqUTXOMap) {
bsqUTXOMap.values().stream().forEach(utxo -> {
Script script = utxo.getScript();
if (!utxoSetByScriptMap.containsKey(script))
utxoSetByScriptMap.put(script, new HashSet<>());
String address = utxo.getAddress();
if (!utxoSetByAddressMap.containsKey(address))
utxoSetByAddressMap.put(address, new HashSet<>());
utxoSetByScriptMap.get(script).add(utxo);
utxoSetByAddressMap.get(address).add(utxo);
});
}
@ -77,12 +76,11 @@ class BsqCoinSelector extends BisqDefaultCoinSelector {
}
@Override
protected boolean selectOutput(TransactionOutput transactionOutput) {
Script scriptPubKey = transactionOutput.getScriptPubKey();
if (scriptPubKey.isSentToAddress() || scriptPubKey.isPayToScriptHash()) {
return utxoSetByScriptMap.containsKey(scriptPubKey);
protected boolean selectOutput(TransactionOutput output) {
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
return utxoSetByAddressMap.containsKey(WalletUtils.getAddressStringFromOutput(output));
} else {
log.warn("transactionOutput.getScriptPubKey() not isSentToAddress or isPayToScriptHash");
log.warn("output.getScriptPubKey() not isSentToAddress or isPayToScriptHash");
return false;
}
}

View file

@ -23,14 +23,11 @@ import io.bisq.common.handlers.ResultHandler;
import io.bisq.core.btc.Restrictions;
import io.bisq.core.btc.exceptions.TransactionVerificationException;
import io.bisq.core.btc.exceptions.WalletException;
import io.bisq.core.dao.blockchain.BsqBlock;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.dao.blockchain.BsqUTXOMap;
import io.bisq.core.dao.blockchain.Tx;
import io.bisq.core.provider.fee.FeeService;
import io.bisq.core.user.Preferences;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@ -40,8 +37,11 @@ import org.bitcoinj.wallet.CoinSelection;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
@ -113,7 +113,7 @@ public class BsqWalletService extends WalletService {
});
});
bsqBlockchainManager.getBsqBlocks().addListener((ListChangeListener<BsqBlock>) c -> updateWalletBsqTransactions());
bsqBlockchainManager.getBsqUTXOMap().addListener(c -> updateWalletBsqTransactions());
}
@ -146,7 +146,7 @@ public class BsqWalletService extends WalletService {
public void requestBsqUtxo(@Nullable ResultHandler resultHandler) {
if (bsqBlockchainManager.isUtxoAvailable()) {
applyUtxoSetToUTXOProvider(bsqBlockchainManager.getUtxoByTxIdMap());
applyUtxoSetToUTXOProvider(bsqBlockchainManager.getBsqUTXOMap());
if (resultHandler != null)
resultHandler.handleResult();
} else {
@ -163,14 +163,40 @@ public class BsqWalletService extends WalletService {
}
private void updateWalletBsqTransactions() {
Set<String> txIdsFromUTXOSet = bsqBlockchainManager.getBsqBlocks().stream()
.flatMap(bsqBlock -> bsqBlock.getTxByTxIdMap().values().stream())
.map(Tx::getId)
.collect(Collectors.toSet());
walletBsqTransactions.setAll(getTxsWithOutputsFoundInBsqTxo());
}
walletBsqTransactions.setAll(getTransactions(true).stream()
.filter(t -> txIdsFromUTXOSet.contains(t.getHashAsString()))
.collect(Collectors.toSet()));
private Set<Transaction> getTxsWithOutputsFoundInBsqTxo() {
return getTransactions(true).stream()
.flatMap(tx -> tx.getOutputs().stream())
.filter(out -> out.getParentTransaction() != null && bsqBlockchainManager.getBsqTXOMap()
.containsTuple(out.getParentTransaction().getHashAsString(), out.getIndex()))
.map(TransactionOutput::getParentTransaction)
.collect(Collectors.toSet());
}
public Set<Transaction> getInvalidBsqTransactions() {
Set<Transaction> txsWithOutputsFoundInBsqTxo = getTxsWithOutputsFoundInBsqTxo();
Set<Transaction> walletTxs = getTransactions(true).stream().collect(Collectors.toSet());
checkArgument(walletTxs.size() >= txsWithOutputsFoundInBsqTxo.size(),
"We cannot have more txsWithOutputsFoundInBsqTxo than walletTxs");
if (walletTxs.size() > txsWithOutputsFoundInBsqTxo.size()) {
Map<String, Transaction> map = walletTxs.stream()
.collect(Collectors.toMap(Transaction::getHashAsString, Function.identity()));
Set<String> walletTxIds = walletTxs.stream()
.map(Transaction::getHashAsString).collect(Collectors.toSet());
Set<String> bsqTxIds = txsWithOutputsFoundInBsqTxo.stream()
.map(Transaction::getHashAsString).collect(Collectors.toSet());
walletTxIds.stream()
.filter(bsqTxIds::contains)
.forEach(map::remove);
return new HashSet<>(map.values());
} else {
return new HashSet<>();
}
}

View file

@ -75,15 +75,14 @@ class BtcCoinSelector extends BisqDefaultCoinSelector {
}
@Override
protected boolean selectOutput(TransactionOutput transactionOutput) {
if (transactionOutput.getScriptPubKey().isSentToAddress() || transactionOutput.getScriptPubKey().isPayToScriptHash()) {
Address address = transactionOutput.getScriptPubKey().getToAddress(params);
log.trace(address.toString());
protected boolean selectOutput(TransactionOutput output) {
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
Address address = WalletUtils.getAddressFromOutput(output);
boolean matchesAddress = addresses.contains(address);
if (!matchesAddress)
log.trace("No match found at matchesRequiredAddress address / addressEntry " +
address.toString() + " / " + address.toString());
log.trace("addresses not containing address " +
addresses + " / " + address.toString());
return matchesAddress;
} else {

View file

@ -27,7 +27,6 @@ import io.bisq.core.btc.exceptions.WalletException;
import io.bisq.core.btc.listeners.AddressConfidenceListener;
import io.bisq.core.btc.listeners.BalanceListener;
import io.bisq.core.btc.listeners.TxConfidenceListener;
import io.bisq.core.dao.blockchain.TxOutput;
import io.bisq.core.provider.fee.FeeService;
import io.bisq.core.user.Preferences;
import org.bitcoinj.core.*;
@ -206,32 +205,6 @@ public abstract class WalletService {
// Sign tx
///////////////////////////////////////////////////////////////////////////////////////////
//TODOcheck with signTransactionInput
/* protected void signInput(Transaction transaction) throws SigningException {
List<TransactionInput> inputs = transaction.getInputs();
int inputIndex = transaction.getInputs().size() - 1;
TransactionInput input = transaction.getInput(inputIndex);
checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null");
Script scriptPubKey = input.getConnectedOutput().getScriptPubKey();
checkNotNull(wallet);
ECKey sigKey = input.getOutpoint().getConnectedKey(wallet);
checkNotNull(sigKey, "signInput: sigKey must not be null. input.getOutpoint()=" + input.getOutpoint().toString());
if (sigKey.isEncrypted())
checkNotNull(aesKey);
Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false);
ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey);
TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false);
if (scriptPubKey.isSentToRawPubKey()) {
input.setScriptSig(ScriptBuilder.createInputScript(txSig));
} else if (scriptPubKey.isSentToAddress()) {
input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey));
} else {
throw new SigningException("Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey);
}
}*/
void signTransactionInput(Transaction tx, TransactionInput txIn, int index) {
KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(wallet, aesKey);
if (txIn.getConnectedOutput() != null) {
@ -331,16 +304,12 @@ public abstract class WalletService {
}
protected 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());
}
});
List<TransactionConfidence> transactionConfidenceList = getOutputsWithConnectedOutputs(tx)
.stream()
.filter(WalletUtils::isOutputScriptConvertableToAddress)
.filter(output -> address.equals(WalletUtils.getAddressFromOutput(output)))
.map(o -> tx.getConfidence())
.collect(Collectors.toList());
return getMostRecentConfidence(transactionConfidenceList);
}
@ -398,12 +367,10 @@ public abstract class WalletService {
protected 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());
}
for (TransactionOutput output : transactionOutputs) {
if (WalletUtils.isOutputScriptConvertableToAddress(output) &&
address.equals(WalletUtils.getAddressFromOutput(output)))
balance = balance.add(output.getValue());
}
return balance;
}
@ -412,12 +379,10 @@ public abstract class WalletService {
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++;
}
for (TransactionOutput output : transactionOutputs) {
if (WalletUtils.isOutputScriptConvertableToAddress(output) &&
address.equals(WalletUtils.getAddressFromOutput(output)))
outputs++;
}
return outputs;
}
@ -545,7 +510,7 @@ public abstract class WalletService {
return transactionOutput.isMine(wallet);
}
public boolean isTxOutputMine(TxOutput txOutput) {
/* public boolean isTxOutputMine(TxOutput txOutput) {
try {
Script script = txOutput.getScript();
if (script.isSentToRawPubKey()) {
@ -563,7 +528,7 @@ public abstract class WalletService {
log.debug("Could not parse tx output script: {}", e.toString());
return false;
}
}
}*/
public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException {
return transaction.getValueSentFromMe(wallet);

View file

@ -0,0 +1,53 @@
/*
* 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.bisq.core.btc.wallet;
import io.bisq.core.btc.BitcoinNetwork;
import io.bisq.core.user.Preferences;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.TransactionOutput;
import javax.annotation.Nullable;
public class WalletUtils {
public static NetworkParameters getParams() {
return Preferences.INSTANCE.getBitcoinNetwork().getParameters();
}
public static boolean isOutputScriptConvertableToAddress(TransactionOutput output) {
return output.getScriptPubKey().isSentToAddress() ||
output.getScriptPubKey().isPayToScriptHash();
}
@Nullable
public static Address getAddressFromOutput(TransactionOutput output) {
return isOutputScriptConvertableToAddress(output) ?
output.getScriptPubKey().getToAddress(getParams()) : null;
}
@Nullable
public static String getAddressStringFromOutput(TransactionOutput output) {
return isOutputScriptConvertableToAddress(output) ?
output.getScriptPubKey().getToAddress(getParams()).toString() : null;
}
public static boolean isRegTest() {
return Preferences.INSTANCE.getBitcoinNetwork().equals(BitcoinNetwork.REGTEST);
}
}

View file

@ -25,4 +25,8 @@ public class BsqBlockchainException extends Exception {
public BsqBlockchainException(String message, Throwable cause) {
super(message, cause);
}
public BsqBlockchainException(Throwable cause) {
super(cause);
}
}

View file

@ -23,9 +23,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import io.bisq.common.UserThread;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.util.Tuple3;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import io.bisq.common.util.Tuple2;
import io.bisq.core.btc.wallet.WalletUtils;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@ -38,21 +37,24 @@ import java.util.Set;
public class BsqBlockchainManager {
private static final Logger log = LoggerFactory.getLogger(BsqBlockchainManager.class);
private final BsqBlockchainService blockchainService;
// regtest
public static final String GENESIS_TX_ID = "3bc7bc9484e112ec8ddd1a1c984379819245ac463b9ce40fa8b5bf771c0f9236";
public static final int GENESIS_BLOCK_HEIGHT = 102;
//mainnet
private static final String GENESIS_TX_ID = "cabbf6073aea8f22ec678e973ac30c6d8fc89321011da6a017f63e67b9f66667";
private static final int GENESIS_BLOCK_HEIGHT = 105301;
private static final String REG_TEST_GENESIS_TX_ID = "3bc7bc9484e112ec8ddd1a1c984379819245ac463b9ce40fa8b5bf771c0f9236";
private static final int REG_TEST_GENESIS_BLOCK_HEIGHT = 102;
private BsqUTXOMap utxoByTxIdMap;
private final List<BsqUTXOListener> bsqUTXOListeners = new ArrayList<>();
private boolean isUtxoAvailable;
@Getter
private final BsqUTXOMap bsqUTXOMap = new BsqUTXOMap();
@Getter
private final BsqTXOMap bsqTXOMap = new BsqTXOMap();
@Getter
private int chainHeadHeight;
// We prefer a list over a set. See: http://stackoverflow.com/questions/24799125/what-is-the-observableset-equivalent-for-setall-method-from-observablelist
@Getter
private final ObservableList<BsqBlock> bsqBlocks = FXCollections.observableArrayList();
private boolean isUtxoAvailable;
private final BsqBlockchainService blockchainService;
private final List<BsqUTXOListener> bsqUTXOListeners = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////
@ -64,7 +66,6 @@ public class BsqBlockchainManager {
this.blockchainService = blockchainService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@ -73,10 +74,14 @@ public class BsqBlockchainManager {
blockchainService.setup(this::setupComplete, errorMessageHandler);
}
public BsqUTXOMap getUtxoByTxIdMap() {
return utxoByTxIdMap;
public Set<String> getUtxoTxIdSet() {
return bsqUTXOMap.getTxIdSet();
}
public Set<String> getTxoTxIdSet() {
return bsqTXOMap.getTxIdSet();
}
public void addUtxoListener(BsqUTXOListener bsqUTXOListener) {
bsqUTXOListeners.add(bsqUTXOListener);
@ -86,40 +91,33 @@ public class BsqBlockchainManager {
bsqUTXOListeners.remove(bsqUTXOListener);
}
public boolean isUtxoAvailable() {
return isUtxoAvailable;
}
///////////////////////////////////////////////////////////////////////////////////////////
// private methods
///////////////////////////////////////////////////////////////////////////////////////////
private void setupComplete() {
ListenableFuture<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>> future =
blockchainService.syncFromGenesis(GENESIS_BLOCK_HEIGHT, GENESIS_TX_ID);
Futures.addCallback(future, new FutureCallback<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>>() {
ListenableFuture<Tuple2<BsqUTXOMap, Integer>> future =
blockchainService.syncFromGenesis(bsqUTXOMap, bsqTXOMap, getGenesisBlockHeight(), getGenesisTxId());
Futures.addCallback(future, new FutureCallback<Tuple2<BsqUTXOMap, Integer>>() {
@Override
public void onSuccess(Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer> tuple) {
public void onSuccess(Tuple2<BsqUTXOMap, Integer> tuple) {
UserThread.execute(() -> {
BsqBlockchainManager.this.bsqBlocks.setAll(tuple.first);
BsqBlockchainManager.this.utxoByTxIdMap = tuple.second;
chainHeadHeight = tuple.third;
chainHeadHeight = tuple.second;
isUtxoAvailable = true;
bsqUTXOListeners.stream().forEach(e -> e.onBsqUTXOChanged(utxoByTxIdMap));
blockchainService.syncFromGenesisCompete(GENESIS_TX_ID,
GENESIS_BLOCK_HEIGHT,
bsqUTXOListeners.stream().forEach(e -> e.onBsqUTXOChanged(bsqUTXOMap));
blockchainService.syncFromGenesisCompete(bsqUTXOMap, bsqTXOMap, getGenesisTxId(),
getGenesisBlockHeight(),
btcdBlock -> {
if (btcdBlock != null) {
UserThread.execute(() -> {
try {
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
blockchainService.parseBlock(bsqBlock,
GENESIS_BLOCK_HEIGHT,
GENESIS_TX_ID,
utxoByTxIdMap);
if (!BsqBlockchainManager.this.bsqBlocks.contains(bsqBlock))
BsqBlockchainManager.this.bsqBlocks.add(bsqBlock);
getGenesisBlockHeight(),
getGenesisTxId(),
bsqUTXOMap,
bsqTXOMap);
} catch (BsqBlockchainException e) {
//TODO
e.printStackTrace();
@ -136,4 +134,12 @@ public class BsqBlockchainManager {
}
});
}
private String getGenesisTxId() {
return WalletUtils.isRegTest() ? REG_TEST_GENESIS_TX_ID : GENESIS_TX_ID;
}
private int getGenesisBlockHeight() {
return WalletUtils.isRegTest() ? REG_TEST_GENESIS_BLOCK_HEIGHT : GENESIS_BLOCK_HEIGHT;
}
}

View file

@ -33,7 +33,6 @@ import io.bisq.common.UserThread;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.handlers.ResultHandler;
import io.bisq.common.util.Tuple2;
import io.bisq.common.util.Tuple3;
import io.bisq.common.util.Utilities;
import io.bisq.core.dao.RpcOptionKeys;
import org.apache.http.impl.client.CloseableHttpClient;
@ -49,7 +48,6 @@ import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -114,9 +112,14 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
return client;
} catch (IOException | BitcoindException | CommunicationException e) {
log.error(e.toString());
e.printStackTrace();
log.error(e.getCause() != null ? e.getCause().toString() : "e.getCause()=null");
throw new BsqBlockchainException(e.getMessage(), e);
}
} catch (Throwable e) {
log.error(e.toString());
e.printStackTrace();
throw new BsqBlockchainException(e.getMessage(), e);
}
});
@ -139,18 +142,24 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
}
@Override
protected ListenableFuture<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId) {
protected ListenableFuture<Tuple2<BsqUTXOMap, Integer>> syncFromGenesis(BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int genesisBlockHeight,
String genesisTxId) {
return rpcRequestsExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
int chainHeadHeight = requestChainHeadHeight();
Tuple2<Set<BsqBlock>, BsqUTXOMap> tuple2 = parseAllBlocksFromGenesis(chainHeadHeight, genesisBlockHeight, genesisTxId);
parseAllBlocksFromGenesis(bsqUTXOMap, bsqTXOMap, chainHeadHeight, genesisBlockHeight, genesisTxId);
log.info("syncFromGenesis took {} ms", System.currentTimeMillis() - startTs);
return new Tuple3<>(tuple2.first, tuple2.second, chainHeadHeight);
return new Tuple2<>(bsqUTXOMap, chainHeadHeight);
});
}
@Override
protected void syncFromGenesisCompete(String genesisTxId, int genesisBlockHeight, Consumer<Block> onNewBlockHandler) {
protected void syncFromGenesisCompete(BsqUTXOMap bsqUTXOMap, BsqTXOMap bsqTXOMap,
String genesisTxId,
int genesisBlockHeight,
Consumer<Block> onNewBlockHandler) {
daemon.addBlockListener(new BlockListener() {
@Override
public void blockDetected(Block block) {
@ -196,10 +205,25 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
final List<TxOutput> txOutputs = rawTransaction.getVOut()
.stream()
.filter(e -> e != null && e.getN() != null && e.getValue() != null && e.getScriptPubKey() != null)
.map(e -> new TxOutput(e.getN(),
e.getValue().movePointRight(8).longValue(),
e.getScriptPubKey().getAddresses(),
e.getScriptPubKey().getHex() != null ? new Script(HEX.decode(e.getScriptPubKey().getHex())) : null))
.map(e -> {
byte[] scriptProgramBytes = new byte[]{};
try {
scriptProgramBytes = e.getScriptPubKey().getHex() != null ?
new Script(HEX.decode(e.getScriptPubKey().getHex())).getProgram() : null;
} catch (Throwable t) {
// expected for tx: 0f8e9655b29d76c5e01147704a64faf95a9c90433baacdd39d4c1d2667e570a2
log.error(e.getScriptPubKey().getAsm());
log.error(e.getScriptPubKey().getHex());
log.error(e.getScriptPubKey().toString());
log.error(t.toString());
t.printStackTrace();
}
return new TxOutput(e.getN(),
e.getValue().movePointRight(8).longValue(),
e.getScriptPubKey().getAddresses(),
scriptProgramBytes,
rawTransaction.getTxId());
})
.collect(Collectors.toList());
// rawTransaction.getTime() is in seconds but we keep it in ms internally

View file

@ -40,7 +40,7 @@ public class BsqBlockchainRpcServiceMain {
BsqBlockchainRpcService blockchainRpcService = new BsqBlockchainRpcService(rpcUser, rpcPassword,
rpcPort, rpcBlockPort, rpcWalletPort);
BsqBlockchainManager bsqBlockchainManager = new BsqBlockchainManager(blockchainRpcService);
bsqBlockchainManager.onAllServicesInitialized(errorMessage -> log.error(errorMessage));
bsqBlockchainManager.onAllServicesInitialized(log::error);
while (true) {
}

View file

@ -26,8 +26,8 @@ import com.neemre.btcdcli4j.core.domain.Block;
import io.bisq.common.app.DevEnv;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.handlers.ResultHandler;
import io.bisq.common.util.Profiler;
import io.bisq.common.util.Tuple2;
import io.bisq.common.util.Tuple3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -56,9 +56,9 @@ abstract public class BsqBlockchainService {
abstract void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler);
abstract ListenableFuture<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId);
abstract ListenableFuture<Tuple2<BsqUTXOMap, Integer>> syncFromGenesis(BsqUTXOMap bsqUTXOMap, BsqTXOMap bsqTXOMap, int genesisBlockHeight, String genesisTxId);
abstract void syncFromGenesisCompete(String genesisTxId, int genesisBlockHeight, Consumer<Block> onNewBlockHandler);
abstract void syncFromGenesisCompete(BsqUTXOMap bsqUTXOMap, BsqTXOMap bsqTXOMap, String genesisTxId, int genesisBlockHeight, Consumer<Block> onNewBlockHandler);
abstract int requestChainHeadHeight() throws BitcoindException, CommunicationException;
@ -67,44 +67,66 @@ abstract public class BsqBlockchainService {
abstract Tx requestTransaction(String txId) throws BsqBlockchainException;
@VisibleForTesting
Tuple2<Set<BsqBlock>, BsqUTXOMap> parseAllBlocksFromGenesis(int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId) throws BsqBlockchainException {
BsqUTXOMap parseAllBlocksFromGenesis(BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId) throws BsqBlockchainException {
try {
BsqUTXOMap bsqUTXOMap = new BsqUTXOMap();
Set<BsqBlock> blocks = new HashSet<>();
log.info("chainHeadHeight=" + chainHeadHeight);
long startTs = System.currentTimeMillis();
long startTotalTs = System.currentTimeMillis();
for (int height = genesisBlockHeight; height <= chainHeadHeight; height++) {
long startBlockTs = System.currentTimeMillis();
Block btcdBlock = requestBlock(height);
log.info("Current block height=" + height);
// 1 block has about 3 MB
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
blocks.add(bsqBlock);
String oldBsqUTXOMap = bsqUTXOMap.toString();
parseBlock(bsqBlock,
genesisBlockHeight,
genesisTxId,
bsqUTXOMap);
log.info(bsqUTXOMap.toString());
bsqUTXOMap,
bsqTXOMap);
String newBsqUTXOMap = bsqUTXOMap.toString();
if (!oldBsqUTXOMap.equals(newBsqUTXOMap))
log.info(bsqUTXOMap.toString());
log.info("Parsing for block {} took {} ms. Total: {} ms for {} blocks",
height,
(System.currentTimeMillis() - startBlockTs),
(System.currentTimeMillis() - startTotalTs),
(height - genesisBlockHeight));
Profiler.printSystemLoad(log);
}
log.info("Took {} ms", System.currentTimeMillis() - startTs);
return new Tuple2<>(blocks, bsqUTXOMap);
log.info("Parsing for all blocks since genesis took {} ms", System.currentTimeMillis() - startTotalTs);
return bsqUTXOMap;
} catch (Throwable t) {
throw new BsqBlockchainException(t.getMessage(), t);
log.error(t.toString());
t.printStackTrace();
throw new BsqBlockchainException(t);
}
}
void parseBlock(BsqBlock block,
int genesisBlockHeight,
String genesisTxId,
BsqUTXOMap bsqUTXOMap)
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap)
throws BsqBlockchainException {
int blockHeight = block.getHeight();
log.debug("Parse block at height={} ", blockHeight);
// We add all transactions to the block
// TODO here we hve the performance bottleneck. takes about 4 sec.
// check if there is more efficient rpc calls for tx ranges or all txs in a block with btcd 14
List<String> txIds = block.getTxIds();
//long startTs = System.currentTimeMillis();
for (String txId : txIds) {
block.addTx(requestTransaction(txId));
}
//log.info("requestTransaction took {} ms for {} txs", System.currentTimeMillis() - startTs, txIds.size());
// First we check for the genesis tx
// All outputs of genesis are valid BSQ UTXOs
@ -112,30 +134,29 @@ abstract public class BsqBlockchainService {
if (blockHeight == genesisBlockHeight) {
txByTxIdMap.entrySet().stream()
.filter(entry -> entry.getKey().equals(genesisTxId))
.forEach(entry -> parseGenesisTx(entry.getValue(), blockHeight, bsqUTXOMap));
.forEach(entry -> parseGenesisTx(entry.getValue(), blockHeight, bsqUTXOMap, bsqTXOMap));
}
// Worst case is that all txs in a block are depending on another, so only once get resolved at each iteration.
// Min tx size is 189 bytes (normally about 240 bytes), 1 MB can contain max. about 5300 txs (usually 2000).
// Realistically we don't expect more then a few recursive calls.
updateBsqUtxoMapFromBlock(txByTxIdMap.values(), bsqUTXOMap, blockHeight, 0, 5300);
updateBsqUtxoMapFromBlock(txByTxIdMap.values(), bsqUTXOMap, bsqTXOMap, blockHeight, 0, 5300);
}
// Recursive method
// Performance-wise the recursion does not hurt (e.g. 5-20 ms).
void updateBsqUtxoMapFromBlock(Collection<Tx> transactions,
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap,
int blockHeight,
int recursionCounter,
int maxRecursions) {
if (recursionCounter > 10)
log.warn("Unusual high recursive calls at resolveConnectedTxs. recursionCounter=" + recursionCounter);
// The set of txIds of txs which are used for inputs of another tx in same block
Set<String> intraBlockSpendingTxIdSet = getIntraBlockSpendingTxIdSet(transactions);
Set<Tx> txsWithoutInputsFromSameBlock = new HashSet<>();
Set<Tx> txsWithInputsFromSameBlock = new HashSet<>();
// First we find the txs which have no intra-block inputs
outerLoop:
for (Tx tx : transactions) {
@ -154,10 +175,19 @@ abstract public class BsqBlockchainService {
txsWithoutInputsFromSameBlock.add(tx);
}
// Usual values is up to 25
// There are soem blocks where it seems devs have tested many depending txs, but even
// those dont exceed 200 recursions and are old blocks from 2012 or so...
// TOD) check strategy btc core uses (sorting the dependency graph would be an optimisation)
if (recursionCounter > 30) {
log.warn("Unusual high recursive calls at resolveConnectedTxs. recursionCounter=" + recursionCounter);
log.warn("txsWithoutInputsFromSameBlock " + txsWithoutInputsFromSameBlock.size());
log.warn("txsWithInputsFromSameBlock " + txsWithInputsFromSameBlock.size());
}
// we check if we have any valid BSQ utxo from that tx set
if (!txsWithoutInputsFromSameBlock.isEmpty()) {
for (Tx tx : txsWithoutInputsFromSameBlock) {
updateBsqUtxoMapFromTx(tx, blockHeight, bsqUTXOMap);
updateBsqUtxoMapFromTx(tx, blockHeight, bsqUTXOMap, bsqTXOMap);
}
}
@ -167,7 +197,7 @@ abstract public class BsqBlockchainService {
// TODO recursion risk?
if (!txsWithInputsFromSameBlock.isEmpty()) {
if (recursionCounter < maxRecursions) {
updateBsqUtxoMapFromBlock(txsWithInputsFromSameBlock, bsqUTXOMap, blockHeight, ++recursionCounter, maxRecursions);
updateBsqUtxoMapFromBlock(txsWithInputsFromSameBlock, bsqUTXOMap, bsqTXOMap, blockHeight, ++recursionCounter, maxRecursions);
} else {
final String msg = "We exceeded our max. recursions for resolveConnectedTxs.\n" +
"txsWithInputsFromSameBlock=" + txsWithInputsFromSameBlock.toString() + "\n" +
@ -177,7 +207,7 @@ abstract public class BsqBlockchainService {
throw new RuntimeException(msg);
}
} else {
log.info("We have no more txsWithInputsFromSameBlock.");
log.debug("We have no more txsWithInputsFromSameBlock.");
}
}
@ -193,7 +223,8 @@ abstract public class BsqBlockchainService {
private boolean updateBsqUtxoMapFromTx(Tx tx,
int blockHeight,
BsqUTXOMap bsqUTXOMap) {
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap) {
String txId = tx.getId();
List<TxOutput> outputs = tx.getOutputs();
boolean utxoChanged = false;
@ -222,12 +253,16 @@ abstract public class BsqBlockchainService {
TxOutput txOutput = outputs.get(outputIndex);
availableValue = availableValue - txOutput.getValue();
if (availableValue >= 0) {
if (txOutput.getAddresses().size() != 1) {
final String msg = "We got a address list with more or less than 1 address for a BsqUTXO. " +
"Seems to be a raw MS. Raw MS are not supported with BSQ.\n" + this.toString();
log.warn(msg);
if (DevEnv.DEV_MODE)
throw new RuntimeException(msg);
}
// We are spending available tokens
BsqUTXO bsqUTXO = new BsqUTXO(txId,
blockHeight,
false,
txOutput);
bsqUTXOMap.putByTuple(txId, outputIndex, bsqUTXO);
bsqUTXOMap.add(new BsqUTXO(txOutput, blockHeight, false));
bsqTXOMap.add(txOutput);
if (availableValue == 0) {
log.debug("We don't have anymore BSQ to spend");
@ -255,7 +290,10 @@ abstract public class BsqBlockchainService {
@VisibleForTesting
void parseGenesisTx(Tx tx, int blockHeight, BsqUTXOMap bsqUTXOMap) {
void parseGenesisTx(Tx tx,
int blockHeight,
BsqUTXOMap bsqUTXOMap,
BsqTXOMap bsqTXOMap) {
String txId = tx.getId();
List<TxOutput> outputs = tx.getOutputs();
@ -263,11 +301,15 @@ abstract public class BsqBlockchainService {
// Genesis tx uses all outputs as BSQ outputs
for (int index = 0; index < outputs.size(); index++) {
TxOutput txOutput = outputs.get(index);
BsqUTXO bsqUTXO = new BsqUTXO(txId,
blockHeight,
true,
txOutput);
bsqUTXOMap.putByTuple(txId, index, bsqUTXO);
if (txOutput.getAddresses().size() != 1) {
final String msg = "We got a address list with more or less than 1 address. " +
"Seems to be a raw MS. Raw MS are not supported with BSQ.\n" + this.toString();
log.warn(msg);
if (DevEnv.DEV_MODE)
throw new RuntimeException(msg);
}
bsqUTXOMap.add(new BsqUTXO(txOutput, blockHeight, true));
bsqTXOMap.add(txOutput);
}
checkArgument(!bsqUTXOMap.isEmpty(), "Genesis tx need to have BSQ utxo when parsing genesis block");
}

View file

@ -0,0 +1,79 @@
/*
* 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.bisq.core.dao.blockchain;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import lombok.extern.slf4j.Slf4j;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
// Map of any ever existing TxOutput which was a valid BSQ
@Slf4j
public class BsqTXOMap {
// We don't use a Lombok delegate here as we want control the access to our map
private ObservableMap<TxIdIndexTuple, TxOutput> map = FXCollections.observableHashMap();
private Set<String> txIdSet = new HashSet<>();
public boolean containsTuple(String txId, int index) {
return map.containsKey(new TxIdIndexTuple(txId, index));
}
public Object add(TxOutput txOutput) {
txIdSet.add(txOutput.getTxId());
return map.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
}
public TxOutput getByTuple(String txId, int index) {
return map.get(new TxIdIndexTuple(txId, index));
}
public void addListener(MapChangeListener<TxIdIndexTuple, TxOutput> listener) {
map.addListener(listener);
}
@Override
public String toString() {
return "BsqUTXOMap " + map.toString();
}
public Collection<TxOutput> values() {
return map.values();
}
public Set<String> getTxIdSet() {
return txIdSet;
}
public boolean isEmpty() {
return map.isEmpty();
}
public int size() {
return map.size();
}
public Set<Map.Entry<TxIdIndexTuple, TxOutput>> entrySet() {
return map.entrySet();
}
}

View file

@ -18,7 +18,7 @@
package io.bisq.core.dao.blockchain;
import lombok.Value;
import org.bitcoinj.script.Script;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@ -26,52 +26,48 @@ import java.util.List;
// 1000 UTXOs - 10 000 UTXOs: 78kb -780kb
@Value
@Slf4j
public class BsqUTXO {
private final String txId;
private final long index;
private final long value;
private final int height;
private final boolean isBsqCoinBase;
private final Script script;
private final TxOutput output;
private final String utxoId;
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for BSQ but lets see if is needed anyway, might be removed
private final List<String> addresses;
private BsqUTXO(String txId, int index, long value, int height, boolean isBsqCoinBase, Script script, List<String> addresses) {
this.txId = txId;
this.index = index;
this.value = value;
public BsqUTXO(TxOutput output, int height, boolean isBsqCoinBase) {
this.height = height;
this.isBsqCoinBase = isBsqCoinBase;
this.script = script;
this.addresses = addresses;
this.output = output;
utxoId = txId + ":" + index;
utxoId = output.getTxId() + ":" + output.getIndex();
}
public BsqUTXO(String txId, int height, boolean isBsqCoinBase, TxOutput output) {
this(txId,
output.getIndex(),
output.getValue(),
height,
isBsqCoinBase,
output.getScript(),
output.getAddresses());
public String getAddress() {
// Only at raw MS outputs addresses have more then 1 entry
// We do not support raw MS for BSQ but lets see if is needed anyway, might be removed
final List<String> addresses = output.getAddresses();
return addresses.size() == 1 ? addresses.get(0) : addresses.toString();
}
public long getValue() {
return output.getValue();
}
public String getTxId() {
return output.getTxId();
}
public int getIndex() {
return output.getIndex();
}
@Override
public String toString() {
return "BsqUTXO{" +
"\n txId='" + txId + '\'' +
",\n index=" + index +
",\n value=" + value +
"\n output='" + output + '\'' +
",\n height=" + height +
",\n isBsqCoinBase=" + isBsqCoinBase +
",\n script=" + script +
",\n utxoId='" + utxoId + '\'' +
",\n addresses=" + addresses +
"\n}";
}
}

View file

@ -17,41 +17,65 @@
package io.bisq.core.dao.blockchain;
import lombok.Value;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import java.util.HashMap;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class BsqUTXOMap {
// We don't use a Lombok delegate here as we want control the access to our map
private ObservableMap<TxIdIndexTuple, BsqUTXO> map = FXCollections.observableHashMap();
private Set<String> txIdSet = new HashSet<>();
public class BsqUTXOMap extends HashMap<BsqUTXOMap.TxIdIndexTuple, BsqUTXO> {
public boolean containsTuple(String txId, int index) {
return super.containsKey(new TxIdIndexTuple(txId, index));
return map.containsKey(new TxIdIndexTuple(txId, index));
}
public Object putByTuple(String txId, int index, BsqUTXO bsqUTXO) {
return super.put(new TxIdIndexTuple(txId, index), bsqUTXO);
public Object add(BsqUTXO bsqUTXO) {
txIdSet.add(bsqUTXO.getTxId());
return map.put(new TxIdIndexTuple(bsqUTXO.getTxId(), bsqUTXO.getIndex()), bsqUTXO);
}
public BsqUTXO getByTuple(String txId, int index) {
return super.get(new TxIdIndexTuple(txId, index));
return map.get(new TxIdIndexTuple(txId, index));
}
public BsqUTXO removeByTuple(String txId, int index) {
return super.remove(new TxIdIndexTuple(txId, index));
txIdSet.remove(txId);
return map.remove(new TxIdIndexTuple(txId, index));
}
public void addListener(MapChangeListener<TxIdIndexTuple, BsqUTXO> listener) {
map.addListener(listener);
}
@Override
public String toString() {
return "BsqUTXOMap " + super.toString();
return "BsqUTXOMap " + map.toString();
}
@Value
static class TxIdIndexTuple {
private final String txId;
private final int index;
public Collection<BsqUTXO> values() {
return map.values();
}
@Override
public String toString() {
return "\nTxIdIndexTuple(" + txId + ":" + index + ")";
}
public Set<String> getTxIdSet() {
return txIdSet;
}
public boolean isEmpty() {
return map.isEmpty();
}
public int size() {
return map.size();
}
public Set<Map.Entry<TxIdIndexTuple, BsqUTXO>> entrySet() {
return map.entrySet();
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.bisq.core.dao.blockchain;
import lombok.Value;
@Value
public class TxIdIndexTuple {
private final String txId;
private final int index;
@Override
public String toString() {
return "\nTxIdIndexTuple(" + txId + ":" + index + ")";
}
}

View file

@ -18,7 +18,7 @@
package io.bisq.core.dao.blockchain;
import lombok.Value;
import org.bitcoinj.script.Script;
import org.bouncycastle.util.encoders.Hex;
import java.util.List;
@ -27,15 +27,16 @@ public class TxOutput {
private final int index;
private final long value;
private final List<String> addresses;
private final Script script;
private final byte[] scriptProgramBytes;
private final String txId;
@Override
public String toString() {
return "TxOutput{" +
"\nindex=" + index +
",\nvalue=" + value +
",\naddresses=" + addresses +
",\nscript=" + script +
",\nscriptProgramBytes=" + Hex.toHexString(scriptProgramBytes) +
"}\n";
}
}

View file

@ -38,6 +38,7 @@ public class ProvidersRepository {
//providers = "http://t4wlzy7l6k4hnolg.onion/, http://g27szt7aw2vrtowe.onion/";
}
}
providerArray = providers.replace(" ", "").split(",");
int index = new Random().nextInt(providerArray.length);
baseUrl = providerArray[index];

View file

@ -20,7 +20,6 @@ package io.bisq.core.dao.blockchain;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.domain.*;
import io.bisq.common.util.Tuple2;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.junit.After;
@ -63,9 +62,13 @@ public class BsqBlockchainServiceTest {
public static final long ADDRESS_TX_2_VALUE = Coin.parseCoin("0.00001000").value;
private MockBsqBlockchainService service;
private BsqUTXOMap bsqUTXOMap;
private BsqTXOMap bsqTXOMap;
@Before
public void setup() {
bsqUTXOMap = new BsqUTXOMap();
bsqTXOMap = new BsqTXOMap();
service = new MockBsqBlockchainService();
}
@ -88,7 +91,7 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_0);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(GEN_TX_ID, 1);
@ -127,7 +130,7 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_1);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX1_ID, 0);
@ -179,7 +182,7 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_1);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX2_ID, 0);
@ -231,7 +234,7 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_2);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX2_ID, 0);
@ -285,7 +288,7 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_1);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(TX2_ID, 0);
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(TX2_ID, 0));
@ -378,9 +381,11 @@ public class BsqBlockchainServiceTest {
}
private Tuple2<Set<BsqBlock>, BsqUTXOMap> parseAllBlocksFromGenesis()
private BsqUTXOMap parseAllBlocksFromGenesis()
throws BitcoindException, CommunicationException, BsqBlockchainException {
return service.parseAllBlocksFromGenesis(service.requestChainHeadHeight(),
return service.parseAllBlocksFromGenesis(bsqUTXOMap,
bsqTXOMap,
service.requestChainHeadHeight(),
BLOCK_0,
GEN_TX_ID);
}

View file

@ -245,7 +245,7 @@ public class SeedWordsView extends ActivatableView<GridPane, Void> {
() -> UserThread.execute(() -> {
log.info("Wallets restored with seed words");
new Popup().feedback(Res.get("seed.restore.success"))
.useShutDownButton();
.useShutDownButton().show();
}),
throwable -> UserThread.execute(() -> {
log.error(throwable.getMessage());

View file

@ -20,6 +20,8 @@ package io.bisq.gui.main.dao.wallet.tx;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.listeners.TxConfidenceListener;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.btc.wallet.WalletUtils;
import io.bisq.gui.components.indicator.TxConfidenceIndicator;
import io.bisq.gui.util.GUIUtil;
import javafx.scene.control.Tooltip;
@ -42,6 +44,7 @@ class BsqTxListItem {
@Getter
private final Transaction transaction;
private BsqWalletService bsqWalletService;
private BtcWalletService btcWalletService;
@Getter
private final Date date;
@Getter
@ -61,9 +64,12 @@ class BsqTxListItem {
private TxConfidenceListener txConfidenceListener;
public BsqTxListItem(Transaction transaction, BsqWalletService bsqWalletService) {
public BsqTxListItem(Transaction transaction,
BsqWalletService bsqWalletService,
BtcWalletService btcWalletService) {
this.transaction = transaction;
this.bsqWalletService = bsqWalletService;
this.btcWalletService = btcWalletService;
txId = transaction.getHashAsString();
date = transaction.getUpdateTime();
@ -88,16 +94,32 @@ class BsqTxListItem {
amount = Coin.ZERO;
direction = "";
}
String result = null;
String foreignReceiverAddress = null;
for (TransactionOutput output : transaction.getOutputs()) {
if (!bsqWalletService.isTransactionOutputMine(output)) {
if (output.getScriptPubKey().isSentToAddress()
|| output.getScriptPubKey().isPayToScriptHash()) {
result = output.getScriptPubKey().getToAddress(bsqWalletService.getParams()).toString();
if (!bsqWalletService.isTransactionOutputMine(output) &&
!btcWalletService.isTransactionOutputMine(output) &&
WalletUtils.isOutputScriptConvertableToAddress(output)) {
// We don't support send txs with multiple outputs to multiple receivers, so we can
// assume that only one output is not from our own wallets.
foreignReceiverAddress = WalletUtils.getAddressStringFromOutput(output);
break;
}
}
// In the case we sent to ourselves (either to BSQ or BTC wallet) we show the first as the other is
// usually the change output.
String ownReceiverAddress = Res.get("shared.na");
if (foreignReceiverAddress != null) {
for (TransactionOutput output : transaction.getOutputs()) {
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
ownReceiverAddress = WalletUtils.getAddressStringFromOutput(output);
break;
}
}
}
address = result != null ? result : "";
address = foreignReceiverAddress != null ? foreignReceiverAddress : ownReceiverAddress;
}
private void setupConfidence(BsqWalletService bsqWalletService) {

View file

@ -20,12 +20,15 @@ package io.bisq.gui.main.dao.wallet.tx;
import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.user.Preferences;
import io.bisq.gui.common.view.ActivatableView;
import io.bisq.gui.common.view.FxmlView;
import io.bisq.gui.components.AddressWithIconAndDirection;
import io.bisq.gui.components.HyperlinkWithIcon;
import io.bisq.gui.main.dao.wallet.BalanceUtil;
import io.bisq.gui.main.overlays.popups.Popup;
import io.bisq.gui.util.BsqFormatter;
import io.bisq.gui.util.GUIUtil;
import io.bisq.gui.util.Layout;
@ -54,6 +57,8 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
private int gridRow = 0;
private BsqFormatter bsqFormatter;
private BsqWalletService bsqWalletService;
private BsqBlockchainManager bsqBlockchainManager;
private BtcWalletService btcWalletService;
private BalanceUtil balanceUtil;
private Preferences preferences;
private ListChangeListener<Transaction> walletBsqTransactionsListener;
@ -67,9 +72,12 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
@Inject
private BsqTxView(BsqFormatter bsqFormatter, BsqWalletService bsqWalletService,
BalanceUtil balanceUtil, Preferences preferences) {
BsqBlockchainManager bsqBlockchainManager,
BtcWalletService btcWalletService, BalanceUtil balanceUtil, Preferences preferences) {
this.bsqFormatter = bsqFormatter;
this.bsqWalletService = bsqWalletService;
this.bsqBlockchainManager = bsqBlockchainManager;
this.btcWalletService = btcWalletService;
this.balanceUtil = balanceUtil;
this.preferences = preferences;
}
@ -120,9 +128,22 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
observableList.forEach(BsqTxListItem::cleanup);
Set<BsqTxListItem> list = bsqWalletService.getWalletBsqTransactions().stream()
.map(t -> new BsqTxListItem(t, bsqWalletService))
.map(t -> new BsqTxListItem(t, bsqWalletService, btcWalletService))
.collect(Collectors.toSet());
observableList.setAll(list);
final Set<Transaction> invalidBsqTransactions = bsqWalletService.getInvalidBsqTransactions();
if (!invalidBsqTransactions.isEmpty() && bsqBlockchainManager.isUtxoAvailable()) {
Set<String> txIds = invalidBsqTransactions.stream().map(t -> t.getHashAsString()).collect(Collectors.toSet());
String key = "invalidBsqTransactionsWarning_" + txIds;
if (preferences.showAgain(key))
new Popup().warning("We detected invalid Bsq transactions.\n" +
"This must not happen if you used the bisq application only to send or receive BSQ.\n\n" +
"invalidBsqTransactionIds=" + txIds.toString())
.width(800)
.dontShowAgainId(key, preferences)
.show();
}
}
private void addDateColumn() {

View file

@ -21,6 +21,7 @@ import io.bisq.common.locale.Res;
import io.bisq.core.btc.listeners.TxConfidenceListener;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.btc.wallet.WalletUtils;
import io.bisq.core.offer.Offer;
import io.bisq.core.offer.OpenOffer;
import io.bisq.core.trade.Tradable;
@ -90,9 +91,9 @@ class TransactionsListItem {
txFeeForBsqPayment = true;
} else {
direction = Res.get("funds.tx.direction.sentTo");
if (output.getScriptPubKey().isSentToAddress()
|| output.getScriptPubKey().isPayToScriptHash()) {
addressString = output.getScriptPubKey().getToAddress(btcWalletService.getParams()).toString();
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
addressString = WalletUtils.getAddressStringFromOutput(output);
break;
}
}
}
@ -102,11 +103,10 @@ class TransactionsListItem {
direction = Res.get("funds.tx.direction.receivedWith");
received = true;
for (TransactionOutput output : transaction.getOutputs()) {
if (!btcWalletService.isTransactionOutputMine(output)) {
if (output.getScriptPubKey().isSentToAddress() ||
output.getScriptPubKey().isPayToScriptHash()) {
addressString = output.getScriptPubKey().getToAddress(btcWalletService.getParams()).toString();
}
if (btcWalletService.isTransactionOutputMine(output) &&
WalletUtils.isOutputScriptConvertableToAddress(output)) {
addressString = WalletUtils.getAddressStringFromOutput(output);
break;
}
}
} else {
@ -119,9 +119,9 @@ class TransactionsListItem {
txFeeForBsqPayment = true;
} else {
outgoing = true;
if (output.getScriptPubKey().isSentToAddress()
|| output.getScriptPubKey().isPayToScriptHash()) {
addressString = output.getScriptPubKey().getToAddress(btcWalletService.getParams()).toString();
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
addressString = WalletUtils.getAddressStringFromOutput(output);
break;
}
}
}
@ -207,7 +207,6 @@ class TransactionsListItem {
dateString = formatter.formatDateTime(date);
}
public void cleanup() {
btcWalletService.removeTxConfidenceListener(txConfidenceListener);
}

View file

@ -335,8 +335,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
// spending tx
// MS tx
candidates.addAll(transaction.getOutputs().stream()
.filter(transactionOutput -> !btcWalletService.isTransactionOutputMine(transactionOutput))
.filter(transactionOutput -> transactionOutput.getScriptPubKey().isPayToScriptHash())
.filter(output -> !btcWalletService.isTransactionOutputMine(output))
.filter(output -> output.getScriptPubKey().isPayToScriptHash())
.map(transactionOutput -> transaction)
.collect(Collectors.toList()));
}

View file

@ -762,9 +762,9 @@ public class Connection implements MessageListener {
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE);
return;
}
} catch (IOException e) {
} catch (Throwable t) {
if (!sharedModel.stopped) {
log.error("Invalid data arrived at inputHandler of connection " + connection, e);
log.error("Invalid data arrived at inputHandler of connection " + connection, t);
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE);
}
return;