mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-20 10:22:18 +01:00
Remove UTXOMap and use TxoMap instead. Cleanup thread handling. Refactor parser and services. Remove RPC_WALLET_NOTIFICATION_PORT option.
This commit is contained in:
parent
c9917998d8
commit
a24ca8edaf
@ -77,7 +77,7 @@ public class BisqEnvironment extends StandardEnvironment {
|
||||
private final String logLevel, providers;
|
||||
private BitcoinNetwork bitcoinNetwork;
|
||||
private final String btcNodes, seedNodes, ignoreDevMsg, useTorForBtc, rpcUser, rpcPassword,
|
||||
rpcPort, rpcBlockNotificationPort, rpcWalletNotificationPort, dumpBlockchainData,
|
||||
rpcPort, rpcBlockNotificationPort, dumpBlockchainData,
|
||||
myAddress, banList, dumpStatistics, maxMemory, socks5ProxyBtcAddress,
|
||||
socks5ProxyHttpAddress;
|
||||
|
||||
@ -181,9 +181,6 @@ public class BisqEnvironment extends StandardEnvironment {
|
||||
rpcBlockNotificationPort = commandLineProperties.containsProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) ?
|
||||
(String) commandLineProperties.getProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) :
|
||||
"";
|
||||
rpcWalletNotificationPort = commandLineProperties.containsProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT) ?
|
||||
(String) commandLineProperties.getProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT) :
|
||||
"";
|
||||
dumpBlockchainData = commandLineProperties.containsProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) ?
|
||||
(String) commandLineProperties.getProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) :
|
||||
"";
|
||||
@ -261,7 +258,6 @@ public class BisqEnvironment extends StandardEnvironment {
|
||||
setProperty(RpcOptionKeys.RPC_PASSWORD, rpcPassword);
|
||||
setProperty(RpcOptionKeys.RPC_PORT, rpcPort);
|
||||
setProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT, rpcBlockNotificationPort);
|
||||
setProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT, rpcWalletNotificationPort);
|
||||
setProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA, dumpBlockchainData);
|
||||
|
||||
setProperty(BtcOptionKeys.BTC_NODES, btcNodes);
|
||||
|
@ -169,9 +169,6 @@ public abstract class BisqExecutable {
|
||||
parser.accepts(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT,
|
||||
description("Bitcoind rpc port for block notifications", ""))
|
||||
.withRequiredArg();
|
||||
parser.accepts(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT,
|
||||
description("Bitcoind rpc port for wallet notifications", ""))
|
||||
.withRequiredArg();
|
||||
parser.accepts(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA,
|
||||
description("If set to true the blockchain data from RPC requests to Bitcoin Core are stored " +
|
||||
"as json file in the data dir.", false))
|
||||
|
@ -17,51 +17,29 @@
|
||||
|
||||
package io.bisq.core.btc.wallet;
|
||||
|
||||
import io.bisq.core.dao.blockchain.BsqUTXO;
|
||||
import io.bisq.core.dao.blockchain.BsqUTXOMap;
|
||||
import io.bisq.core.dao.blockchain.TxOutputMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation.
|
||||
* We lookup for spendable outputs which matches our address of our address.
|
||||
*/
|
||||
@Slf4j
|
||||
class BsqCoinSelector extends BisqDefaultCoinSelector {
|
||||
private static final Logger log = LoggerFactory.getLogger(BsqCoinSelector.class);
|
||||
|
||||
private final Map<String, Set<BsqUTXO>> utxoSetByAddressMap = new HashMap<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
private TxOutputMap txOutputMap;
|
||||
|
||||
public BsqCoinSelector(boolean permitForeignPendingTx) {
|
||||
super(permitForeignPendingTx);
|
||||
}
|
||||
|
||||
public void setUtxoMap(BsqUTXOMap bsqUTXOMap) {
|
||||
bsqUTXOMap.values().stream().forEach(utxo -> {
|
||||
String address = utxo.getAddress();
|
||||
if (!utxoSetByAddressMap.containsKey(address))
|
||||
utxoSetByAddressMap.put(address, new HashSet<>());
|
||||
|
||||
utxoSetByAddressMap.get(address).add(utxo);
|
||||
});
|
||||
public void setTxoMap(TxOutputMap txOutputMap) {
|
||||
this.txOutputMap = txOutputMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isTxOutputSpendable(TransactionOutput output) {
|
||||
if (WalletUtils.isOutputScriptConvertableToAddress(output)) {
|
||||
return utxoSetByAddressMap.containsKey(WalletUtils.getAddressStringFromOutput(output));
|
||||
} else {
|
||||
log.warn("output.getScriptPubKey() not isSentToAddress or isPayToScriptHash");
|
||||
return false;
|
||||
}
|
||||
return output.getParentTransaction() != null &&
|
||||
txOutputMap.isTxOutputUnSpent(output.getParentTransaction().getHashAsString(), output.getIndex());
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ 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.BsqBlockchainManager;
|
||||
import io.bisq.core.dao.blockchain.BsqUTXOMap;
|
||||
import io.bisq.core.dao.blockchain.TxOutput;
|
||||
import io.bisq.core.provider.fee.FeeService;
|
||||
import io.bisq.core.user.Preferences;
|
||||
import javafx.collections.FXCollections;
|
||||
@ -42,6 +42,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@ -51,7 +52,7 @@ public class BsqWalletService extends WalletService {
|
||||
private final BsqBlockchainManager bsqBlockchainManager;
|
||||
private final BsqCoinSelector bsqCoinSelector;
|
||||
@Getter
|
||||
private final ObservableList<Transaction> walletBsqTransactions = FXCollections.observableArrayList();
|
||||
private final ObservableList<Transaction> walletTransactions = FXCollections.observableArrayList();
|
||||
private final CopyOnWriteArraySet<BsqBalanceListener> bsqBalanceListeners = new CopyOnWriteArraySet<>();
|
||||
private Coin availableBsqBalance;
|
||||
|
||||
@ -77,6 +78,7 @@ public class BsqWalletService extends WalletService {
|
||||
wallet.setCoinSelector(bsqCoinSelector);
|
||||
|
||||
wallet.addEventListener(walletEventListener);
|
||||
|
||||
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
|
||||
@ -90,7 +92,7 @@ public class BsqWalletService extends WalletService {
|
||||
|
||||
@Override
|
||||
public void onReorganize(Wallet wallet) {
|
||||
updateWalletBsqTransactions();
|
||||
updateBsqWalletTransactions();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -99,29 +101,25 @@ public class BsqWalletService extends WalletService {
|
||||
|
||||
@Override
|
||||
public void onKeysAdded(List<ECKey> keys) {
|
||||
updateWalletBsqTransactions();
|
||||
updateBsqWalletTransactions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {
|
||||
updateWalletBsqTransactions();
|
||||
updateBsqWalletTransactions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWalletChanged(Wallet wallet) {
|
||||
updateWalletBsqTransactions();
|
||||
updateBsqWalletTransactions();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
bsqBlockchainManager.addUtxoListener(bsqUTXOMap -> {
|
||||
updateCoinSelector(bsqUTXOMap);
|
||||
updateWalletBsqTransactions();
|
||||
updateBsqBalance();
|
||||
});
|
||||
bsqBlockchainManager.getBsqTXOMap().addBurnedBSQTxMapListener(change -> {
|
||||
updateWalletBsqTransactions();
|
||||
bsqBlockchainManager.addTxOutputMapListener(bsqTxoMap -> {
|
||||
bsqCoinSelector.setTxoMap(bsqTxoMap);
|
||||
updateBsqWalletTransactions();
|
||||
updateBsqBalance();
|
||||
});
|
||||
}
|
||||
@ -144,6 +142,11 @@ public class BsqWalletService extends WalletService {
|
||||
// Balance
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void updateBsqBalance() {
|
||||
availableBsqBalance = bsqCoinSelector.select(NetworkParameters.MAX_MONEY, getMyUnspentBsqTxOutputsFromWallet()).valueGathered;
|
||||
bsqBalanceListeners.stream().forEach(e -> e.updateAvailableBalance(availableBsqBalance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Coin getAvailableBalance() {
|
||||
return availableBsqBalance;
|
||||
@ -157,66 +160,56 @@ public class BsqWalletService extends WalletService {
|
||||
bsqBalanceListeners.remove(listener);
|
||||
}
|
||||
|
||||
private void updateBsqBalance() {
|
||||
availableBsqBalance = bsqCoinSelector.select(NetworkParameters.MAX_MONEY, getMyBsqUtxosFromWallet()).valueGathered;
|
||||
bsqBalanceListeners.stream().forEach(e -> e.updateAvailableBalance(availableBsqBalance));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UTXO
|
||||
// BSQ TransactionOutputs and Transactions
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void updateCoinSelector(BsqUTXOMap bsqUTXOMap) {
|
||||
bsqCoinSelector.setUtxoMap(bsqUTXOMap);
|
||||
private void updateBsqWalletTransactions() {
|
||||
walletTransactions.setAll(getAllBsqTransactionsFromWallet());
|
||||
}
|
||||
|
||||
private void updateWalletBsqTransactions() {
|
||||
walletBsqTransactions.setAll(getWalletTransactionsWithBsqTxo());
|
||||
}
|
||||
|
||||
private Set<TransactionOutput> getAllBsqUtxosFromWallet() {
|
||||
return getTransactions(true).stream()
|
||||
.flatMap(tx -> tx.getOutputs().stream())
|
||||
.filter(out -> out.getParentTransaction() != null && bsqBlockchainManager.getBsqUTXOMap()
|
||||
.containsTuple(out.getParentTransaction().getHashAsString(), out.getIndex()))
|
||||
private Set<TransactionOutput> getAllBsqTxOutputsFromWallet() {
|
||||
return getWalletTransactionOutputStream()
|
||||
.filter(out -> out.getParentTransaction() != null && bsqBlockchainManager.getTxOutputMap()
|
||||
.contains(out.getParentTransaction().getHashAsString(), out.getIndex()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Set<TransactionOutput> getMyBsqUtxosFromWallet() {
|
||||
return getAllBsqUtxosFromWallet().stream()
|
||||
.filter(out -> out.isMine(wallet))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Set<TransactionOutput> getWalletBsqTxos() {
|
||||
return getTransactions(true).stream()
|
||||
.flatMap(tx -> tx.getOutputs().stream())
|
||||
private Set<TransactionOutput> getAllUnspentBsqTxOutputsFromWallet() {
|
||||
return getAllBsqTxOutputsFromWallet().stream()
|
||||
.filter(out -> {
|
||||
final Transaction parentTransaction = out.getParentTransaction();
|
||||
if (parentTransaction == null)
|
||||
final Transaction tx = out.getParentTransaction();
|
||||
if (tx == null) {
|
||||
return false;
|
||||
final String id = parentTransaction.getHashAsString();
|
||||
return bsqBlockchainManager.getBsqTXOMap().containsTuple(id, out.getIndex()) ||
|
||||
bsqBlockchainManager.getBsqTXOMap().getBurnedBSQTxMap().containsKey(id);
|
||||
} else {
|
||||
final TxOutput txOutput = bsqBlockchainManager.getTxOutputMap()
|
||||
.get(tx.getHashAsString(), out.getIndex());
|
||||
return txOutput != null && txOutput.isUnSpend();
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Set<Transaction> getWalletTransactionsWithBsqUtxo() {
|
||||
return getAllBsqUtxosFromWallet().stream()
|
||||
.map(TransactionOutput::getParentTransaction)
|
||||
private Set<TransactionOutput> getMyUnspentBsqTxOutputsFromWallet() {
|
||||
return getAllUnspentBsqTxOutputsFromWallet().stream()
|
||||
.filter(out -> out.isMine(wallet))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Set<Transaction> getWalletTransactionsWithBsqTxo() {
|
||||
return getWalletBsqTxos().stream()
|
||||
private Stream<TransactionOutput> getWalletTransactionOutputStream() {
|
||||
return getTransactions(true).stream()
|
||||
.flatMap(tx -> tx.getOutputs().stream());
|
||||
}
|
||||
|
||||
private Set<Transaction> getAllBsqTransactionsFromWallet() {
|
||||
return getAllBsqTxOutputsFromWallet().stream()
|
||||
.map(TransactionOutput::getParentTransaction)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public Set<Transaction> getInvalidBsqTransactions() {
|
||||
Set<Transaction> txsWithOutputsFoundInBsqTxo = getWalletTransactionsWithBsqTxo();
|
||||
Set<Transaction> txsWithOutputsFoundInBsqTxo = getAllBsqTransactionsFromWallet();
|
||||
Set<Transaction> walletTxs = getTransactions(true).stream().collect(Collectors.toSet());
|
||||
checkArgument(walletTxs.size() >= txsWithOutputsFoundInBsqTxo.size(),
|
||||
"We cannot have more txsWithOutputsFoundInBsqTxo than walletTxs");
|
||||
@ -323,7 +316,7 @@ public class BsqWalletService extends WalletService {
|
||||
// non dust BTC output.
|
||||
|
||||
// TODO check dust output
|
||||
CoinSelection coinSelection = bsqCoinSelector.select(fee, getMyBsqUtxosFromWallet());
|
||||
CoinSelection coinSelection = bsqCoinSelector.select(fee, getMyUnspentBsqTxOutputsFromWallet());
|
||||
coinSelection.gathered.stream().forEach(tx::addInput);
|
||||
Coin change = bsqCoinSelector.getChange(fee, coinSelection);
|
||||
if (change.isPositive())
|
||||
|
@ -64,7 +64,7 @@ public abstract class WalletService {
|
||||
protected final Preferences preferences;
|
||||
protected final FeeService feeService;
|
||||
protected final NetworkParameters params;
|
||||
protected final WalletEventListener walletEventListener = new BisqWalletEventListener();
|
||||
protected final WalletEventListener walletEventListener = new BisqWalletListener();
|
||||
protected final CopyOnWriteArraySet<AddressConfidenceListener> addressConfidenceListeners = new CopyOnWriteArraySet<>();
|
||||
protected final CopyOnWriteArraySet<TxConfidenceListener> txConfidenceListeners = new CopyOnWriteArraySet<>();
|
||||
protected final CopyOnWriteArraySet<BalanceListener> balanceListeners = new CopyOnWriteArraySet<>();
|
||||
@ -563,7 +563,7 @@ public abstract class WalletService {
|
||||
// bisqWalletEventListener
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public class BisqWalletEventListener extends AbstractWalletEventListener {
|
||||
public class BisqWalletListener extends AbstractWalletEventListener {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
|
||||
notifyBalanceListeners(tx);
|
||||
|
@ -22,6 +22,7 @@ import io.bisq.common.app.AppModule;
|
||||
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
|
||||
import io.bisq.core.dao.blockchain.BsqBlockchainRpcService;
|
||||
import io.bisq.core.dao.blockchain.BsqBlockchainService;
|
||||
import io.bisq.core.dao.blockchain.json.JsonExporter;
|
||||
import io.bisq.core.dao.compensation.CompensationRequestManager;
|
||||
import io.bisq.core.dao.vote.VotingDefaultValues;
|
||||
import io.bisq.core.dao.vote.VotingManager;
|
||||
@ -44,6 +45,7 @@ public class DaoModule extends AppModule {
|
||||
bind(DaoManager.class).in(Singleton.class);
|
||||
bind(BsqBlockchainManager.class).in(Singleton.class);
|
||||
bind(BsqBlockchainService.class).to(BsqBlockchainRpcService.class).in(Singleton.class);
|
||||
bind(JsonExporter.class).in(Singleton.class);
|
||||
bind(DaoPeriodService.class).in(Singleton.class);
|
||||
bind(VotingService.class).in(Singleton.class);
|
||||
|
||||
@ -57,8 +59,6 @@ public class DaoModule extends AppModule {
|
||||
bindConstant().annotatedWith(named(RpcOptionKeys.RPC_PORT)).to(env.getRequiredProperty(RpcOptionKeys.RPC_PORT));
|
||||
bindConstant().annotatedWith(named(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT))
|
||||
.to(env.getRequiredProperty(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT));
|
||||
bindConstant().annotatedWith(named(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT))
|
||||
.to(env.getRequiredProperty(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT));
|
||||
bindConstant().annotatedWith(named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA))
|
||||
.to(env.getRequiredProperty(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA));
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ public class RpcOptionKeys {
|
||||
public static final String RPC_PASSWORD = "rpcPassword";
|
||||
public static final String RPC_PORT = "rpcPort";
|
||||
public static final String RPC_BLOCK_NOTIFICATION_PORT = "rpcBlockNotificationPort";
|
||||
public static final String RPC_WALLET_NOTIFICATION_PORT = "rpcWalletNotificationPort";
|
||||
|
||||
public static final String DUMP_BLOCKCHAIN_DATA = "dumpBlockchainData";
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -25,11 +24,8 @@ import java.util.List;
|
||||
|
||||
@Value
|
||||
public class BsqBlock {
|
||||
@Getter
|
||||
private final int height;
|
||||
@Getter
|
||||
private final List<String> txIds;
|
||||
// private final Map<String, Tx> txByTxIdMap = new HashMap<>();
|
||||
private final List<Tx> txList = new ArrayList<>();
|
||||
|
||||
public BsqBlock(List<String> txIds, int height) {
|
||||
@ -38,17 +34,12 @@ public class BsqBlock {
|
||||
}
|
||||
|
||||
public void addTx(Tx tx) {
|
||||
//txByTxIdMap.put(tx.getId(), tx);
|
||||
txList.add(tx);
|
||||
}
|
||||
|
||||
/* public Tx getTxByTxId(String txId) {
|
||||
return txByTxIdMap.get(txId);
|
||||
}*/
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BsqBlock{" +
|
||||
return "Block{" +
|
||||
"\nheight=" + height +
|
||||
",\ntxIds=" + txIds +
|
||||
",\ntxList=" + txList +
|
||||
|
@ -17,36 +17,27 @@
|
||||
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.inject.Inject;
|
||||
import com.neemre.btcdcli4j.core.domain.PubKeyScript;
|
||||
import io.bisq.common.UserThread;
|
||||
import io.bisq.common.handlers.ErrorMessageHandler;
|
||||
import io.bisq.common.storage.PlainTextWrapper;
|
||||
import io.bisq.common.storage.Storage;
|
||||
import io.bisq.common.util.Utilities;
|
||||
import io.bisq.core.app.BisqEnvironment;
|
||||
import io.bisq.core.btc.BitcoinNetwork;
|
||||
import io.bisq.core.dao.RpcOptionKeys;
|
||||
import io.bisq.core.dao.blockchain.json.ScriptPubKeyForJson;
|
||||
import io.bisq.core.dao.blockchain.json.SpentInfoForJson;
|
||||
import io.bisq.core.dao.blockchain.json.TxOutputForJson;
|
||||
import io.bisq.core.dao.blockchain.json.JsonExporter;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class BsqBlockchainManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(BsqBlockchainManager.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Static fields
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//mainnet
|
||||
private static final String GENESIS_TX_ID = "cabbf6073aea8f22ec678e973ac30c6d8fc89321011da6a017f63e67b9f66667";
|
||||
@ -65,29 +56,28 @@ public class BsqBlockchainManager {
|
||||
// new snapshot is block 90. We only persist at the new snapshot, so we always re-parse from latest snapshot after
|
||||
// a restart.
|
||||
private static final int SNAPSHOT_TRIGGER = 300000;
|
||||
private final boolean connectToBtcCore;
|
||||
|
||||
public static int getSnapshotTrigger() {
|
||||
return SNAPSHOT_TRIGGER;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Class fields
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private final BsqBlockchainService blockchainService;
|
||||
private File storageDir;
|
||||
private final boolean dumpBlockchainData;
|
||||
private final JsonExporter jsonExporter;
|
||||
private final BitcoinNetwork bitcoinNetwork;
|
||||
private final List<BsqUTXOListener> bsqUTXOListeners = new ArrayList<>();
|
||||
private final List<BsqTxoListener> bsqTxoListeners = new ArrayList<>();
|
||||
private final List<TxOutputMap.Listener> txOutputMapListeners = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
private final BsqUTXOMap bsqUTXOMap;
|
||||
@Getter
|
||||
private final BsqTXOMap bsqTXOMap;
|
||||
private final TxOutputMap txOutputMap;
|
||||
@Getter
|
||||
private int chainHeadHeight;
|
||||
@Getter
|
||||
private boolean isUtxoSyncWithChainHeadHeight;
|
||||
private final Storage<PlainTextWrapper> jsonStorage;
|
||||
private boolean parseBlockchainComplete;
|
||||
private final boolean connectToBtcCore;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -97,211 +87,108 @@ public class BsqBlockchainManager {
|
||||
@Inject
|
||||
public BsqBlockchainManager(BsqBlockchainService blockchainService,
|
||||
BisqEnvironment bisqEnvironment,
|
||||
JsonExporter jsonExporter,
|
||||
@Named(Storage.DIR_KEY) File storageDir,
|
||||
Storage<PlainTextWrapper> jsonStorage,
|
||||
@Named(RpcOptionKeys.RPC_USER) String rpcUser,
|
||||
@Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
|
||||
@Named(RpcOptionKeys.RPC_USER) String rpcUser) {
|
||||
this.blockchainService = blockchainService;
|
||||
this.storageDir = storageDir;
|
||||
this.jsonStorage = jsonStorage;
|
||||
this.dumpBlockchainData = dumpBlockchainData;
|
||||
this.jsonExporter = jsonExporter;
|
||||
this.bitcoinNetwork = bisqEnvironment.getBitcoinNetwork();
|
||||
|
||||
connectToBtcCore = rpcUser != null && !rpcUser.isEmpty();
|
||||
bsqUTXOMap = new BsqUTXOMap(storageDir);
|
||||
bsqTXOMap = new BsqTXOMap(storageDir);
|
||||
txOutputMap = new TxOutputMap(storageDir);
|
||||
|
||||
bsqUTXOMap.addListener(c -> onBsqUTXOChanged());
|
||||
bsqTXOMap.addListener(c -> onBsqTXOChanged());
|
||||
bsqTXOMap.addBurnedBSQTxMapListener(c -> onBsqTXOChanged());
|
||||
|
||||
if (dumpBlockchainData) {
|
||||
this.jsonStorage.initWithFileName("txo.json");
|
||||
/* p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onBootstrapComplete() {
|
||||
addOfferBookChangedListener(new OfferBookChangedListener() {
|
||||
@Override
|
||||
public void onAdded(Offer offer) {
|
||||
doDumpBlockchainData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(Offer offer) {
|
||||
doDumpBlockchainData();
|
||||
}
|
||||
});
|
||||
UserThread.runAfter(BsqBlockchainManager.this::doDumpBlockchainData, 1);
|
||||
}
|
||||
});*/
|
||||
}
|
||||
txOutputMap.addListener(bsqTxOutputMap -> onBsqTxoChanged());
|
||||
}
|
||||
|
||||
private void doDumpBlockchainData() {
|
||||
List<TxOutputForJson> list = bsqTXOMap.getMap().values().stream()
|
||||
.map(this::getTxOutputForJson)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
list.sort((o1, o2) -> (o1.getSortData().compareTo(o2.getSortData())));
|
||||
TxOutputForJson[] array = new TxOutputForJson[list.size()];
|
||||
list.toArray(array);
|
||||
jsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(array)), 5000);
|
||||
|
||||
// keep the individual file storage option as code as we dont know yet what we will use.
|
||||
/* log.error("txOutputForJson " + txOutputForJson);
|
||||
File txoDir = new File(Paths.get(storageDir.getAbsolutePath(), "txo").toString());
|
||||
if (!txoDir.exists())
|
||||
if (!txoDir.mkdir())
|
||||
log.warn("make txoDir failed.\ntxoDir=" + txoDir.getAbsolutePath());
|
||||
File txoFile = new File(Paths.get(txoDir.getAbsolutePath(),
|
||||
txOutput.getTxId() + ":" + outputIndex + ".json").toString());
|
||||
|
||||
// Nr of write requests might be a bit heavy, consider write whole list to one file
|
||||
FileManager<PlainTextWrapper> fileManager = new FileManager<>(storageDir, txoFile, 1);
|
||||
fileManager.saveLater(new PlainTextWrapper(Utilities.objectToJson(txOutputForJson)));*/
|
||||
}
|
||||
|
||||
private TxOutputForJson getTxOutputForJson(TxOutput txOutput) {
|
||||
String txId = txOutput.getTxId();
|
||||
int outputIndex = txOutput.getIndex();
|
||||
final long bsqAmount = txOutput.getValue();
|
||||
final int height = txOutput.getBlockHeight();
|
||||
final boolean isBsqCoinBase = txOutput.isBsqCoinBase();
|
||||
final boolean verified = txOutput.isVerified();
|
||||
final long burnedFee = txOutput.getBurnedFee();
|
||||
final long btcTxFee = txOutput.getBtcTxFee();
|
||||
|
||||
PubKeyScript pubKeyScript = txOutput.getPubKeyScript();
|
||||
final ScriptPubKeyForJson scriptPubKey = new ScriptPubKeyForJson(pubKeyScript.getAddresses(),
|
||||
pubKeyScript.getAsm(),
|
||||
pubKeyScript.getHex(),
|
||||
pubKeyScript.getReqSigs(),
|
||||
pubKeyScript.getType().toString());
|
||||
SpentInfoForJson spentInfoJson = null;
|
||||
SpendInfo spendInfo = txOutput.getSpendInfo();
|
||||
if (spendInfo != null)
|
||||
spentInfoJson = new SpentInfoForJson(spendInfo.getBlockHeight(),
|
||||
spendInfo.getInputIndex(),
|
||||
spendInfo.getTxId());
|
||||
|
||||
final long time = txOutput.getTime();
|
||||
final String txVersion = txOutput.getTxVersion();
|
||||
return new TxOutputForJson(txId,
|
||||
outputIndex,
|
||||
bsqAmount,
|
||||
height,
|
||||
isBsqCoinBase,
|
||||
verified,
|
||||
burnedFee,
|
||||
btcTxFee,
|
||||
scriptPubKey,
|
||||
spentInfoJson,
|
||||
time,
|
||||
txVersion
|
||||
);
|
||||
}
|
||||
|
||||
private void onBsqUTXOChanged() {
|
||||
bsqUTXOListeners.stream().forEach(e -> e.onBsqUTXOChanged(bsqUTXOMap));
|
||||
}
|
||||
|
||||
private void onBsqTXOChanged() {
|
||||
bsqTxoListeners.stream().forEach(e -> e.onBsqTxoChanged(bsqTXOMap));
|
||||
if (dumpBlockchainData)
|
||||
doDumpBlockchainData();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Public methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onAllServicesInitialized(ErrorMessageHandler errorMessageHandler) {
|
||||
if (connectToBtcCore)
|
||||
blockchainService.setup(this::blockchainServiceSetupCompleted, errorMessageHandler);
|
||||
blockchainService.setup(this::onSetupComplete, errorMessageHandler);
|
||||
}
|
||||
|
||||
public Set<String> getUtxoTxIdSet() {
|
||||
return bsqUTXOMap.getTxIdSet();
|
||||
}
|
||||
|
||||
public Set<String> getTxoTxIdSet() {
|
||||
return bsqTXOMap.getTxIdSet();
|
||||
}
|
||||
|
||||
public void addUtxoListener(BsqUTXOListener bsqUTXOListener) {
|
||||
bsqUTXOListeners.add(bsqUTXOListener);
|
||||
}
|
||||
|
||||
public void removeUtxoListener(BsqUTXOListener bsqUTXOListener) {
|
||||
bsqUTXOListeners.remove(bsqUTXOListener);
|
||||
}
|
||||
|
||||
public void addTxoListener(BsqTxoListener bsqTxoListener) {
|
||||
bsqTxoListeners.add(bsqTxoListener);
|
||||
}
|
||||
|
||||
public void removeTxoListener(BsqTxoListener bsqTxoListener) {
|
||||
bsqTxoListeners.remove(bsqTxoListener);
|
||||
public void addTxOutputMapListener(TxOutputMap.Listener listener) {
|
||||
txOutputMapListeners.add(listener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// private methods
|
||||
// Private methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void blockchainServiceSetupCompleted() {
|
||||
private void onSetupComplete() {
|
||||
final int genesisBlockHeight = getGenesisBlockHeight();
|
||||
final String genesisTxId = getGenesisTxId();
|
||||
int startBlockHeight = Math.max(genesisBlockHeight, bsqUTXOMap.getSnapshotHeight());
|
||||
log.info("genesisTxId=" + genesisTxId);
|
||||
log.info("genesisBlockHeight=" + genesisBlockHeight);
|
||||
log.info("startBlockHeight=" + startBlockHeight);
|
||||
log.info("bsqUTXOMap.getSnapshotHeight()=" + bsqUTXOMap.getSnapshotHeight());
|
||||
int startBlockHeight = Math.max(genesisBlockHeight, txOutputMap.getSnapshotHeight());
|
||||
log.info("parseBlocks with: genesisTxId={}\ngenesisBlockHeight={}\nstartBlockHeight={}\nsnapshotHeight={}",
|
||||
genesisTxId, genesisBlockHeight, startBlockHeight, txOutputMap.getSnapshotHeight());
|
||||
|
||||
if (bsqUTXOMap.getSnapshotHeight() > 0)
|
||||
onBsqUTXOChanged();
|
||||
// If we have past data we notify our listeners
|
||||
if (txOutputMap.getSnapshotHeight() > 0)
|
||||
onBsqTxoChanged();
|
||||
|
||||
ListenableFuture<Integer> future =
|
||||
blockchainService.executeParseBlockchain(bsqUTXOMap,
|
||||
bsqTXOMap,
|
||||
startBlockHeight,
|
||||
parseBlocks(startBlockHeight,
|
||||
genesisBlockHeight,
|
||||
genesisTxId);
|
||||
}
|
||||
|
||||
// TODO handle reorgs
|
||||
|
||||
private void parseBlocks(int startBlockHeight, int genesisBlockHeight, String genesisTxId) {
|
||||
blockchainService.requestChainHeadHeight(chainHeadHeight -> {
|
||||
if (chainHeadHeight != startBlockHeight) {
|
||||
blockchainService.parseBlocks(startBlockHeight,
|
||||
chainHeadHeight,
|
||||
genesisBlockHeight,
|
||||
genesisTxId);
|
||||
genesisTxId,
|
||||
txOutputMap,
|
||||
() -> {
|
||||
// we are done but it might be that new blocks have arrived in the meantime,
|
||||
// so we try again with startBlockHeight set to current chainHeadHeight
|
||||
parseBlocks(chainHeadHeight,
|
||||
genesisBlockHeight,
|
||||
genesisTxId);
|
||||
}, throwable -> {
|
||||
log.error(throwable.toString());
|
||||
throwable.printStackTrace();
|
||||
});
|
||||
} else {
|
||||
// We dont have received new blocks in the meantime so we are completed and we register our handler
|
||||
BsqBlockchainManager.this.chainHeadHeight = chainHeadHeight;
|
||||
parseBlockchainComplete = true;
|
||||
|
||||
Futures.addCallback(future, new FutureCallback<Integer>() {
|
||||
@Override
|
||||
public void onSuccess(Integer height) {
|
||||
UserThread.execute(() -> {
|
||||
chainHeadHeight = height;
|
||||
isUtxoSyncWithChainHeadHeight = true;
|
||||
blockchainService.parseBlockchainCompete(btcdBlock -> {
|
||||
if (btcdBlock != null) {
|
||||
UserThread.execute(() -> {
|
||||
try {
|
||||
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
|
||||
blockchainService.parseBlock(bsqBlock,
|
||||
genesisBlockHeight,
|
||||
genesisTxId,
|
||||
bsqUTXOMap,
|
||||
bsqTXOMap);
|
||||
} catch (BsqBlockchainException e) {
|
||||
//TODO
|
||||
e.printStackTrace();
|
||||
}
|
||||
// We register our handler for new blocks
|
||||
blockchainService.addBlockHandler(bsqBlock -> {
|
||||
blockchainService.parseBlock(bsqBlock,
|
||||
genesisBlockHeight,
|
||||
genesisTxId,
|
||||
txOutputMap,
|
||||
() -> {
|
||||
log.debug("new block parsed. bsqBlock={}", bsqBlock);
|
||||
}, throwable -> {
|
||||
log.error(throwable.toString());
|
||||
throwable.printStackTrace();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
UserThread.execute(() -> log.error("syncFromGenesis failed" + throwable.toString()));
|
||||
}
|
||||
}, throwable -> {
|
||||
log.error(throwable.toString());
|
||||
throwable.printStackTrace();
|
||||
});
|
||||
}
|
||||
|
||||
private void onBsqTxoChanged() {
|
||||
txOutputMapListeners.stream().forEach(e -> e.onMapChanged(txOutputMap));
|
||||
jsonExporter.export(txOutputMap);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private String getGenesisTxId() {
|
||||
return bitcoinNetwork == BitcoinNetwork.REGTEST ? REG_TEST_GENESIS_TX_ID : GENESIS_TX_ID;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
@ -25,7 +26,6 @@ import com.google.inject.Inject;
|
||||
import com.neemre.btcdcli4j.core.BitcoindException;
|
||||
import com.neemre.btcdcli4j.core.CommunicationException;
|
||||
import com.neemre.btcdcli4j.core.client.BtcdClientImpl;
|
||||
import com.neemre.btcdcli4j.core.domain.Block;
|
||||
import com.neemre.btcdcli4j.core.domain.RawTransaction;
|
||||
import com.neemre.btcdcli4j.daemon.BtcdDaemonImpl;
|
||||
import com.neemre.btcdcli4j.daemon.event.BlockListener;
|
||||
@ -51,6 +51,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
// We are in threaded context. We map our results to the client into the UserThread to not extend thread contexts.
|
||||
public class BsqBlockchainRpcService extends BsqBlockchainService {
|
||||
private static final Logger log = LoggerFactory.getLogger(BsqBlockchainRpcService.class);
|
||||
|
||||
@ -58,13 +59,13 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
|
||||
private final String rpcPassword;
|
||||
private final String rpcPort;
|
||||
private final String rpcBlockPort;
|
||||
private final String rpcWalletPort;
|
||||
private final ListeningExecutorService setupExecutorService = Utilities.getListeningExecutorService("BlockchainRpcService.setup", 1, 1, 5);
|
||||
private final ListeningExecutorService rpcRequestsExecutor = Utilities.getListeningExecutorService("BlockchainRpcService.requests", 1, 1, 10);
|
||||
|
||||
private final ListeningExecutorService setupExecutor = Utilities.getListeningExecutorService("RpcService.setup", 1, 1, 5);
|
||||
private final ListeningExecutorService parseBlockchainExecutor = Utilities.getListeningExecutorService("RpcService.requests", 1, 1, 10);
|
||||
private final ListeningExecutorService getChainHeightExecutor = Utilities.getListeningExecutorService("RpcService.requests", 1, 1, 10);
|
||||
private BtcdClientImpl client;
|
||||
private BtcdDaemonImpl daemon;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -73,23 +74,21 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
|
||||
public BsqBlockchainRpcService(@Named(RpcOptionKeys.RPC_USER) String rpcUser,
|
||||
@Named(RpcOptionKeys.RPC_PASSWORD) String rpcPassword,
|
||||
@Named(RpcOptionKeys.RPC_PORT) String rpcPort,
|
||||
@Named(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort,
|
||||
@Named(RpcOptionKeys.RPC_WALLET_NOTIFICATION_PORT) String rpcWalletPort) {
|
||||
@Named(RpcOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort) {
|
||||
this.rpcUser = rpcUser;
|
||||
this.rpcPassword = rpcPassword;
|
||||
this.rpcPort = rpcPort;
|
||||
this.rpcBlockPort = rpcBlockPort;
|
||||
this.rpcWalletPort = rpcWalletPort;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Protected methods
|
||||
// Non blocking methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
ListenableFuture<BtcdClientImpl> future = setupExecutorService.submit(() -> {
|
||||
ListenableFuture<BtcdClientImpl> future = setupExecutor.submit(() -> {
|
||||
long startTs = System.currentTimeMillis();
|
||||
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
|
||||
CloseableHttpClient httpProvider = HttpClients.custom().setConnectionManager(cm).build();
|
||||
@ -103,7 +102,6 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
|
||||
nodeConfig.setProperty("node.bitcoind.rpc.password", rpcPassword);
|
||||
nodeConfig.setProperty("node.bitcoind.rpc.port", rpcPort);
|
||||
nodeConfig.setProperty("node.bitcoind.notification.block.port", rpcBlockPort);
|
||||
nodeConfig.setProperty("node.bitcoind.notification.wallet.port", rpcWalletPort);
|
||||
BtcdClientImpl client = new BtcdClientImpl(httpProvider, nodeConfig);
|
||||
daemon = new BtcdDaemonImpl(client);
|
||||
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
|
||||
@ -141,64 +139,119 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<Integer> executeParseBlockchain(BsqUTXOMap bsqUTXOMap,
|
||||
BsqTXOMap bsqTXOMap,
|
||||
int startBlockHeight,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId) {
|
||||
return rpcRequestsExecutor.submit(() -> {
|
||||
long startTs = System.currentTimeMillis();
|
||||
int chainHeadHeight = requestChainHeadHeight();
|
||||
parseBlockchain(bsqUTXOMap,
|
||||
bsqTXOMap,
|
||||
chainHeadHeight,
|
||||
startBlockHeight,
|
||||
genesisBlockHeight,
|
||||
genesisTxId);
|
||||
log.info("syncFromGenesis took {} ms", System.currentTimeMillis() - startTs);
|
||||
return chainHeadHeight;
|
||||
void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler) {
|
||||
ListenableFuture<Integer> future = getChainHeightExecutor.submit(client::getBlockCount);
|
||||
|
||||
Futures.addCallback(future, new FutureCallback<Integer>() {
|
||||
public void onSuccess(Integer chainHeadHeight) {
|
||||
UserThread.execute(() -> resultHandler.accept(chainHeadHeight));
|
||||
}
|
||||
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
UserThread.execute(() -> errorHandler.accept(throwable));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseBlockchainCompete(Consumer<Block> onNewBlockHandler) {
|
||||
daemon.addBlockListener(new BlockListener() {
|
||||
void parseBlocks(int startBlockHeight,
|
||||
int chainHeadHeight,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
TxOutputMap txOutputMap,
|
||||
ResultHandler resultHandler,
|
||||
Consumer<Throwable> errorHandler) {
|
||||
ListenableFuture<Void> future = parseBlockchainExecutor.submit(() -> {
|
||||
long startTs = System.currentTimeMillis();
|
||||
BsqParser bsqParser = new BsqParser(this);
|
||||
bsqParser.parseBlocks(startBlockHeight,
|
||||
chainHeadHeight,
|
||||
genesisBlockHeight,
|
||||
genesisTxId, txOutputMap);
|
||||
log.info("parseBlockchain took {} ms", System.currentTimeMillis() - startTs);
|
||||
return null;
|
||||
});
|
||||
|
||||
Futures.addCallback(future, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void blockDetected(Block block) {
|
||||
log.info("blockDetected " + block.getHash());
|
||||
if (onNewBlockHandler != null)
|
||||
onNewBlockHandler.accept(block);
|
||||
public void onSuccess(Void ignore) {
|
||||
UserThread.execute(resultHandler::handleResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
errorHandler.accept(throwable);
|
||||
}
|
||||
});
|
||||
/* daemon.addWalletListener(new WalletListener() {
|
||||
@Override
|
||||
public void walletChanged(Transaction transaction) {
|
||||
log.info("walletChanged " + transaction.getTxId());
|
||||
*//* try {
|
||||
// parseTransaction(transaction.getTxId(), GENESIS_TX_ID, client.getBlockCount(), tempUtxoByTxIdMap, bsqUTXOMap);
|
||||
printUtxoMap(bsqUTXOMap);
|
||||
} catch (BitcoindException | CommunicationException | BsqBlockchainException e) {
|
||||
//TODO
|
||||
e.printStackTrace();
|
||||
}*//*
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
@Override
|
||||
void parseBlock(BsqBlock block,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
TxOutputMap txOutputMap,
|
||||
ResultHandler resultHandler,
|
||||
Consumer<Throwable> errorHandler) {
|
||||
ListenableFuture<Void> future = parseBlockchainExecutor.submit(() -> {
|
||||
BsqParser bsqParser = new BsqParser(this);
|
||||
bsqParser.parseBlock(block,
|
||||
genesisBlockHeight,
|
||||
genesisTxId,
|
||||
txOutputMap);
|
||||
return null;
|
||||
});
|
||||
|
||||
Futures.addCallback(future, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void ignore) {
|
||||
UserThread.execute(resultHandler::handleResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
errorHandler.accept(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
void addBlockHandler(Consumer<BsqBlock> blockHandler) {
|
||||
daemon.addBlockListener(new BlockListener() {
|
||||
@Override
|
||||
public void blockDetected(com.neemre.btcdcli4j.core.domain.Block btcdBlock) {
|
||||
if (btcdBlock != null) {
|
||||
UserThread.execute(() -> {
|
||||
log.info("new block detected " + btcdBlock.getHash());
|
||||
blockHandler.accept(new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight()));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Blocking methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
int requestChainHeadHeight() throws BitcoindException, CommunicationException {
|
||||
return client.getBlockCount();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
Block requestBlock(int blockHeight) throws BitcoindException, CommunicationException {
|
||||
com.neemre.btcdcli4j.core.domain.Block requestBlock(int blockHeight) throws BitcoindException, CommunicationException {
|
||||
return client.getBlock(client.getBlockHash(blockHeight));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
Tx requestTransaction(String txId, int blockHeight) throws BsqBlockchainException {
|
||||
try {
|
||||
RawTransaction rawTransaction = getRawTransaction(txId);
|
||||
RawTransaction rawTransaction = requestRawTransaction(txId);
|
||||
// rawTransaction.getTime() is in seconds but we keep it in ms internally
|
||||
final long time = rawTransaction.getTime() * 1000;
|
||||
final List<TxInput> txInputs = rawTransaction.getVIn()
|
||||
.stream()
|
||||
@ -209,18 +262,13 @@ 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(rawOutput -> {
|
||||
|
||||
return new TxOutput(rawOutput.getN(),
|
||||
rawOutput.getValue().movePointRight(8).longValue(),
|
||||
rawTransaction.getTxId(),
|
||||
rawOutput.getScriptPubKey(),
|
||||
blockHeight,
|
||||
time);
|
||||
})
|
||||
.map(rawOutput -> new TxOutput(rawOutput.getN(),
|
||||
rawOutput.getValue().movePointRight(8).longValue(),
|
||||
rawTransaction.getTxId(),
|
||||
rawOutput.getScriptPubKey(),
|
||||
blockHeight,
|
||||
time))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// rawTransaction.getTime() is in seconds but we keep it in ms internally
|
||||
return new Tx(txId,
|
||||
txInputs,
|
||||
txOutputs,
|
||||
@ -230,7 +278,10 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
|
||||
}
|
||||
}
|
||||
|
||||
protected RawTransaction getRawTransaction(String txId) throws BitcoindException, CommunicationException {
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
RawTransaction requestRawTransaction(String txId) throws BitcoindException, CommunicationException {
|
||||
return (RawTransaction) client.getRawTransaction(txId, 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,31 +18,18 @@
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.inject.Inject;
|
||||
import com.neemre.btcdcli4j.core.BitcoindException;
|
||||
import com.neemre.btcdcli4j.core.CommunicationException;
|
||||
import com.neemre.btcdcli4j.core.domain.Block;
|
||||
import io.bisq.common.app.DevEnv;
|
||||
import com.neemre.btcdcli4j.core.domain.RawTransaction;
|
||||
import io.bisq.common.handlers.ErrorMessageHandler;
|
||||
import io.bisq.common.handlers.ResultHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@Slf4j
|
||||
abstract public class BsqBlockchainService {
|
||||
private int snapshotHeight;
|
||||
// todo just temp
|
||||
// Map<Integer, String> recursionMap = new HashMap<>();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -54,295 +41,44 @@ abstract public class BsqBlockchainService {
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Protected methods
|
||||
// Non blocking methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
abstract void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler);
|
||||
|
||||
abstract ListenableFuture<Integer> executeParseBlockchain(BsqUTXOMap bsqUTXOMap,
|
||||
BsqTXOMap bsqTXOMap,
|
||||
int startBlockHeight,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId);
|
||||
abstract void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler);
|
||||
|
||||
abstract void parseBlockchainCompete(Consumer<Block> onNewBlockHandler);
|
||||
abstract void parseBlocks(int startBlockHeight,
|
||||
int chainHeadHeight,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
TxOutputMap txOutputMap,
|
||||
ResultHandler resultHandler,
|
||||
Consumer<Throwable> errorHandler);
|
||||
|
||||
abstract void addBlockHandler(Consumer<BsqBlock> onNewBlockHandler);
|
||||
|
||||
abstract void parseBlock(BsqBlock block,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
TxOutputMap txOutputMap,
|
||||
ResultHandler resultHandler,
|
||||
Consumer<Throwable> errorHandler);
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Blocking methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@VisibleForTesting
|
||||
abstract int requestChainHeadHeight() throws BitcoindException, CommunicationException;
|
||||
|
||||
abstract Block requestBlock(int i) throws BitcoindException, CommunicationException;
|
||||
@VisibleForTesting
|
||||
abstract com.neemre.btcdcli4j.core.domain.Block requestBlock(int i) throws BitcoindException, CommunicationException;
|
||||
|
||||
@VisibleForTesting
|
||||
abstract Tx requestTransaction(String txId, int blockHeight) throws BsqBlockchainException;
|
||||
|
||||
@VisibleForTesting
|
||||
void parseBlockchain(BsqUTXOMap bsqUTXOMap,
|
||||
BsqTXOMap bsqTXOMap,
|
||||
int chainHeadHeight,
|
||||
int startBlockHeight,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId) throws BsqBlockchainException {
|
||||
try {
|
||||
log.info("chainHeadHeight=" + chainHeadHeight);
|
||||
long startTotalTs = System.currentTimeMillis();
|
||||
for (int height = startBlockHeight; height <= chainHeadHeight; height++) {
|
||||
long startBlockTs = System.currentTimeMillis();
|
||||
Block btcdBlock = requestBlock(height);
|
||||
log.debug("Current block height=" + height);
|
||||
|
||||
// 1 block has about 3 MB
|
||||
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
|
||||
|
||||
// String oldBsqUTXOMap = bsqUTXOMap.toString();
|
||||
parseBlock(bsqBlock,
|
||||
genesisBlockHeight,
|
||||
genesisTxId,
|
||||
bsqUTXOMap,
|
||||
bsqTXOMap);
|
||||
// String newBsqUTXOMap = bsqUTXOMap.toString();
|
||||
|
||||
/* StringBuilder sb = new StringBuilder("recursionMap:\n");
|
||||
List<String> list = new ArrayList<>();
|
||||
//recursionMap.entrySet().stream().forEach(e -> sb.append(e.getKey()).append(": ").append(e.getValue()).append("\n"));
|
||||
recursionMap.entrySet().stream().forEach(e -> list.add("\nBlock height / Tx graph depth / Nr. of Txs: " + e.getKey()
|
||||
+ " / " + e.getValue()));
|
||||
Collections.sort(list);
|
||||
list.stream().forEach(e -> sb.append(e).append("\n"));
|
||||
log.warn(list.toString().replace(",", "").replace("[", "").replace("]", ""));*/
|
||||
|
||||
/* log.info("Parsing for block {} took {} ms. Total: {} ms for {} blocks",
|
||||
height,
|
||||
(System.currentTimeMillis() - startBlockTs),
|
||||
(System.currentTimeMillis() - startTotalTs),
|
||||
(height - startBlockHeight + 1));
|
||||
Profiler.printSystemLoad(log);*/
|
||||
}
|
||||
log.info("Parsing for all blocks since genesis took {} ms", System.currentTimeMillis() - startTotalTs);
|
||||
} catch (Throwable t) {
|
||||
log.error(t.toString());
|
||||
t.printStackTrace();
|
||||
throw new BsqBlockchainException(t);
|
||||
}
|
||||
}
|
||||
|
||||
void parseBlock(BsqBlock block,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
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();
|
||||
Tx genesisTx = null;
|
||||
for (String txId : txIds) {
|
||||
final Tx tx = requestTransaction(txId, blockHeight);
|
||||
block.addTx(tx);
|
||||
if (txId.equals(genesisTxId))
|
||||
genesisTx = tx;
|
||||
}
|
||||
|
||||
// First we check for the genesis tx
|
||||
// All outputs of genesis are valid BSQ UTXOs
|
||||
if (genesisTx != null) {
|
||||
checkArgument(blockHeight == genesisBlockHeight,
|
||||
"If we have a matching genesis tx the block height must mathc as well");
|
||||
parseGenesisTx(genesisTx, blockHeight, bsqUTXOMap, bsqTXOMap);
|
||||
}
|
||||
//txSize = block.getTxList().size();
|
||||
|
||||
// Worst case is that all txs in a block are depending on another, so only once get resolved at each iteration.
|
||||
// Worst case is that all txs in a block are depending on another, so only one 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.
|
||||
// There are some blocks with testing such dependency chains like block 130768 where at each iteration only
|
||||
// one get resolved.
|
||||
updateBsqUtxoMapFromBlock(block.getTxList(), bsqUTXOMap, bsqTXOMap, blockHeight, 0, 5300);
|
||||
|
||||
int trigger = BsqBlockchainManager.getSnapshotTrigger();
|
||||
if (blockHeight % trigger == 0 && blockHeight > snapshotHeight - trigger) {
|
||||
snapshotHeight = blockHeight - trigger;
|
||||
log.info("We reached a new snapshot trigger at height {}. New snapshotHeight is {}",
|
||||
blockHeight, snapshotHeight);
|
||||
bsqUTXOMap.setSnapshotHeight(snapshotHeight);
|
||||
bsqUTXOMap.persist();
|
||||
bsqTXOMap.setSnapshotHeight(snapshotHeight);
|
||||
bsqTXOMap.persist();
|
||||
}
|
||||
}
|
||||
|
||||
//private int txSize;
|
||||
|
||||
// Recursive method
|
||||
// Performance-wise the recursion does not hurt (e.g. 5-20 ms).
|
||||
void updateBsqUtxoMapFromBlock(List<Tx> transactions,
|
||||
BsqUTXOMap bsqUTXOMap,
|
||||
BsqTXOMap bsqTXOMap,
|
||||
int blockHeight,
|
||||
int recursionCounter,
|
||||
int maxRecursions) {
|
||||
//recursionMap.put(blockHeight, recursionCounter + " / " + txSize);
|
||||
|
||||
// The set of txIds of txs which are used for inputs of another tx in same block
|
||||
Set<String> intraBlockSpendingTxIdSet = getIntraBlockSpendingTxIdSet(transactions);
|
||||
|
||||
List<Tx> txsWithoutInputsFromSameBlock = new ArrayList<>();
|
||||
List<Tx> txsWithInputsFromSameBlock = new ArrayList<>();
|
||||
|
||||
// First we find the txs which have no intra-block inputs
|
||||
outerLoop:
|
||||
for (Tx tx : transactions) {
|
||||
for (TxInput input : tx.getInputs()) {
|
||||
if (intraBlockSpendingTxIdSet.contains(input.getSpendingTxId())) {
|
||||
// We have an input from one of the intra-block-transactions, so we cannot process that tx now.
|
||||
// We add the tx for later parsing to the txsWithInputsFromSameBlock and move to the next
|
||||
// outerLoop iteration .
|
||||
txsWithInputsFromSameBlock.add(tx);
|
||||
|
||||
continue outerLoop;
|
||||
}
|
||||
}
|
||||
// If we have not found any tx input pointing to anther tx in the same block we add it to our
|
||||
// txsWithoutInputsFromSameBlock for first pass of BSQ validation
|
||||
txsWithoutInputsFromSameBlock.add(tx);
|
||||
}
|
||||
|
||||
// Usual values is up to 25
|
||||
// There are some blocks where it seems devs have tested graphs of many depending txs, but even
|
||||
// those dont exceed 200 recursions and are mostly old blocks from 2012 when fees have been low ;-).
|
||||
// TODO check strategy btc core uses (sorting the dependency graph would be an optimisation)
|
||||
// Seems btc core delivers tx list sorted by dependency graph. -> TODO verify and test
|
||||
if (recursionCounter > 100) {
|
||||
log.warn("Unusual high recursive calls at resolveConnectedTxs. recursionCounter=" + recursionCounter);
|
||||
log.warn("blockHeight=" + blockHeight);
|
||||
log.warn("txsWithoutInputsFromSameBlock.size " + txsWithoutInputsFromSameBlock.size());
|
||||
log.warn("txsWithInputsFromSameBlock.size " + txsWithInputsFromSameBlock.size());
|
||||
// if (txsWithoutInputsFromSameBlock.size() == 1)
|
||||
// log.warn("txsWithInputsFromSameBlock " + txsWithInputsFromSameBlock.stream().map(e->e.getId()).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
// we check if we have any valid BSQ utxo from that tx set
|
||||
if (!txsWithoutInputsFromSameBlock.isEmpty()) {
|
||||
for (Tx tx : txsWithoutInputsFromSameBlock) {
|
||||
updateBsqUtxoMapFromTx(tx, blockHeight, bsqUTXOMap, bsqTXOMap);
|
||||
}
|
||||
}
|
||||
|
||||
// we check if we have any valid BSQ utxo from that tx set
|
||||
// We might have InputsFromSameBlock whcih are BTC only but not BSQ, so we cannot
|
||||
// optimize here and need to iterate further.
|
||||
// TODO recursion risk?
|
||||
if (!txsWithInputsFromSameBlock.isEmpty()) {
|
||||
if (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" +
|
||||
"txsWithoutInputsFromSameBlock=" + txsWithoutInputsFromSameBlock;
|
||||
log.warn(msg);
|
||||
if (DevEnv.DEV_MODE)
|
||||
throw new RuntimeException(msg);
|
||||
}
|
||||
} else {
|
||||
log.debug("We have no more txsWithInputsFromSameBlock.");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getIntraBlockSpendingTxIdSet(List<Tx> transactions) {
|
||||
Set<String> txIdSet = transactions.stream().map(Tx::getId).collect(Collectors.toSet());
|
||||
Set<String> intraBlockSpendingTxIdSet = new HashSet<>();
|
||||
transactions.stream()
|
||||
.forEach(tx -> tx.getInputs().stream()
|
||||
.filter(input -> txIdSet.contains(input.getSpendingTxId()))
|
||||
.forEach(input -> intraBlockSpendingTxIdSet.add(input.getSpendingTxId())));
|
||||
return intraBlockSpendingTxIdSet;
|
||||
}
|
||||
|
||||
private boolean updateBsqUtxoMapFromTx(Tx tx,
|
||||
int blockHeight,
|
||||
BsqUTXOMap bsqUTXOMap,
|
||||
BsqTXOMap bsqTXOMap) {
|
||||
List<TxOutput> outputs = tx.getOutputs();
|
||||
boolean utxoChanged = false;
|
||||
long availableValue = 0;
|
||||
long totalAvailableBSQInputs = 0;
|
||||
long totalSpendBSQOutputs = 0;
|
||||
final String txId = tx.getId();
|
||||
for (int inputIndex = 0; inputIndex < tx.getInputs().size(); inputIndex++) {
|
||||
TxInput input = tx.getInputs().get(inputIndex);
|
||||
String spendingTxId = input.getSpendingTxId();
|
||||
final int spendingTxOutputIndex = input.getSpendingTxOutputIndex();
|
||||
|
||||
if (bsqTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) {
|
||||
TxOutput txOutputFromSpendingTx = bsqTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex);
|
||||
txOutputFromSpendingTx.setSpendInfo(new SpendInfo(blockHeight, txId, inputIndex));
|
||||
}
|
||||
|
||||
if (bsqUTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) {
|
||||
BsqUTXO bsqUTXO = bsqUTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex);
|
||||
availableValue = availableValue + bsqUTXO.getValue();
|
||||
totalAvailableBSQInputs += bsqUTXO.getValue();
|
||||
bsqUTXOMap.removeByTuple(spendingTxId, spendingTxOutputIndex);
|
||||
utxoChanged = true;
|
||||
|
||||
if (bsqUTXOMap.isEmpty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an input spending tokens we iterate the outputs
|
||||
if (availableValue > 0) {
|
||||
// We use order of output index. An output is a BSQ utxo as long there is enough input value
|
||||
for (TxOutput txOutput : outputs) {
|
||||
availableValue = availableValue - txOutput.getValue();
|
||||
if (availableValue >= 0) {
|
||||
// We are spending available tokens
|
||||
txOutput.setVerified(true);
|
||||
bsqUTXOMap.add(new BsqUTXO(blockHeight, txOutput, false));
|
||||
bsqTXOMap.add(txOutput);
|
||||
totalSpendBSQOutputs += txOutput.getValue();
|
||||
|
||||
if (availableValue == 0) {
|
||||
log.debug("We don't have anymore BSQ to spend");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
log.debug("We tried to spend more BSQ as we have in our inputs");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final long burnedAmount = totalAvailableBSQInputs - totalSpendBSQOutputs;
|
||||
if (burnedAmount > 0) {
|
||||
log.debug("BSQ have been left which was not spent. Burned BSQ amount={}, tx={}",
|
||||
availableValue,
|
||||
tx.toString());
|
||||
tx.getOutputs().stream().forEach(e -> e.setBurnedFee(burnedAmount));
|
||||
bsqTXOMap.addBurnedBSQTx(tx);
|
||||
}
|
||||
|
||||
return utxoChanged;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void parseGenesisTx(Tx tx,
|
||||
int blockHeight,
|
||||
BsqUTXOMap bsqUTXOMap,
|
||||
BsqTXOMap bsqTXOMap) {
|
||||
List<TxOutput> outputs = tx.getOutputs();
|
||||
|
||||
//TODO use BsqTXO not BsqUTXO as we dont know if they are unspent
|
||||
// Genesis tx uses all outputs as BSQ outputs
|
||||
for (TxOutput txOutput : outputs) {
|
||||
txOutput.setVerified(true);
|
||||
txOutput.setBsqCoinBase(true);
|
||||
bsqUTXOMap.add(new BsqUTXO(blockHeight, txOutput, true));
|
||||
bsqTXOMap.add(txOutput);
|
||||
}
|
||||
checkArgument(!bsqUTXOMap.isEmpty(), "Genesis tx need to have BSQ utxo when parsing genesis block");
|
||||
}
|
||||
abstract RawTransaction requestRawTransaction(String txId) throws BitcoindException, CommunicationException;
|
||||
}
|
||||
|
283
core/src/main/java/io/bisq/core/dao/blockchain/BsqParser.java
Normal file
283
core/src/main/java/io/bisq/core/dao/blockchain/BsqParser.java
Normal file
@ -0,0 +1,283 @@
|
||||
/*
|
||||
* This file is part of bisq.
|
||||
*
|
||||
* bisq 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.
|
||||
*
|
||||
* bisq 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 bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.bisq.common.app.DevEnv;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
// We are in threaded context. Don't mix up with UserThread.
|
||||
@Slf4j
|
||||
@Immutable
|
||||
public class BsqParser {
|
||||
// todo just temp
|
||||
// Map<Integer, String> recursionMap = new HashMap<>();
|
||||
|
||||
private final BsqBlockchainService bsqBlockchainService;
|
||||
|
||||
public BsqParser(BsqBlockchainService bsqBlockchainService) {
|
||||
this.bsqBlockchainService = bsqBlockchainService;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Parsing
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@VisibleForTesting
|
||||
void parseBlocks(int startBlockHeight,
|
||||
int chainHeadHeight,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
TxOutputMap txOutputMap) throws BsqBlockchainException {
|
||||
try {
|
||||
log.info("chainHeadHeight=" + chainHeadHeight);
|
||||
long startTotalTs = System.currentTimeMillis();
|
||||
for (int height = startBlockHeight; height <= chainHeadHeight; height++) {
|
||||
long startBlockTs = System.currentTimeMillis();
|
||||
com.neemre.btcdcli4j.core.domain.Block btcdBlock = bsqBlockchainService.requestBlock(height);
|
||||
log.debug("Current block height=" + height);
|
||||
|
||||
// 1 block has about 3 MB, but we keep it only in memory as long as needed
|
||||
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
|
||||
|
||||
parseBlock(bsqBlock,
|
||||
genesisBlockHeight,
|
||||
genesisTxId,
|
||||
txOutputMap);
|
||||
|
||||
/* StringBuilder sb = new StringBuilder("recursionMap:\n");
|
||||
List<String> list = new ArrayList<>();
|
||||
//recursionMap.entrySet().stream().forEach(e -> sb.append(e.getKey()).append(": ").append(e.getValue()).append("\n"));
|
||||
recursionMap.entrySet().stream().forEach(e -> list.add("\nBlock height / Tx graph depth / Nr. of Txs: " + e.getKey()
|
||||
+ " / " + e.getValue()));
|
||||
Collections.sort(list);
|
||||
list.stream().forEach(e -> sb.append(e).append("\n"));
|
||||
log.warn(list.toString().replace(",", "").replace("[", "").replace("]", ""));*/
|
||||
|
||||
/* log.info("Parsing for block {} took {} ms. Total: {} ms for {} blocks",
|
||||
height,
|
||||
(System.currentTimeMillis() - startBlockTs),
|
||||
(System.currentTimeMillis() - startTotalTs),
|
||||
(height - startBlockHeight + 1));
|
||||
Profiler.printSystemLoad(log);*/
|
||||
}
|
||||
log.info("Parsing for all blocks since genesis took {} ms", System.currentTimeMillis() - startTotalTs);
|
||||
} catch (Throwable t) {
|
||||
log.error(t.toString());
|
||||
t.printStackTrace();
|
||||
throw new BsqBlockchainException(t);
|
||||
}
|
||||
}
|
||||
|
||||
void parseBlock(BsqBlock block,
|
||||
int genesisBlockHeight,
|
||||
String genesisTxId,
|
||||
TxOutputMap txOutputMap)
|
||||
throws BsqBlockchainException {
|
||||
int blockHeight = block.getHeight();
|
||||
log.debug("Parse block at height={} ", blockHeight);
|
||||
// We add all transactions to the block
|
||||
List<String> txIds = block.getTxIds();
|
||||
Tx genesisTx = null;
|
||||
for (String txId : txIds) {
|
||||
final Tx tx = bsqBlockchainService.requestTransaction(txId, blockHeight);
|
||||
block.addTx(tx);
|
||||
if (txId.equals(genesisTxId))
|
||||
genesisTx = tx;
|
||||
}
|
||||
|
||||
if (genesisTx != null) {
|
||||
checkArgument(blockHeight == genesisBlockHeight,
|
||||
"If we have a matching genesis tx the block height must match as well");
|
||||
parseGenesisTx(genesisTx, txOutputMap);
|
||||
}
|
||||
//txSize = block.getTxList().size();
|
||||
|
||||
// Worst case is that all txs in a block are depending on another, so only one 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.
|
||||
// There are some blocks with testing such dependency chains like block 130768 where at each iteration only
|
||||
// one get resolved.
|
||||
// Lately there is a patter with 24 iterations observed
|
||||
parseTransactions(block.getTxList(), txOutputMap, blockHeight, 0, 5300);
|
||||
|
||||
// We persist only at certain snapshots. They have a safety distance from head so re-orgs should not cause
|
||||
// issues in our persisted data.
|
||||
int trigger = BsqBlockchainManager.getSnapshotTrigger();
|
||||
int snapshotHeight = txOutputMap.getSnapshotHeight();
|
||||
if (blockHeight % trigger == 0 && blockHeight > snapshotHeight - trigger) {
|
||||
snapshotHeight = blockHeight - trigger;
|
||||
log.info("We reached a new snapshot trigger at height {}. New snapshotHeight is {}",
|
||||
blockHeight, snapshotHeight);
|
||||
txOutputMap.setSnapshotHeight(snapshotHeight);
|
||||
txOutputMap.persist();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void parseGenesisTx(Tx tx,
|
||||
TxOutputMap txOutputMap) {
|
||||
// Genesis tx uses all outputs as BSQ outputs
|
||||
tx.getOutputs().stream().forEach(txOutput -> {
|
||||
txOutput.setVerified(true);
|
||||
txOutput.setBsqCoinBase(true);
|
||||
txOutputMap.put(txOutput);
|
||||
});
|
||||
}
|
||||
|
||||
// Recursive method
|
||||
// Performance-wise the recursion does not hurt (e.g. 5-20 ms).
|
||||
// The RPC requestTransaction is the slow call.
|
||||
void parseTransactions(List<Tx> transactions,
|
||||
TxOutputMap txOutputMap,
|
||||
int blockHeight,
|
||||
int recursionCounter,
|
||||
int maxRecursions) {
|
||||
//recursionMap.put(blockHeight, recursionCounter + " / " + txSize);
|
||||
|
||||
// The set of txIds of txs which are used for inputs of another tx in same block
|
||||
Set<String> intraBlockSpendingTxIdSet = getIntraBlockSpendingTxIdSet(transactions);
|
||||
|
||||
List<Tx> txsWithoutInputsFromSameBlock = new ArrayList<>();
|
||||
List<Tx> txsWithInputsFromSameBlock = new ArrayList<>();
|
||||
|
||||
// First we find the txs which have no intra-block inputs
|
||||
outerLoop:
|
||||
for (Tx tx : transactions) {
|
||||
for (TxInput input : tx.getInputs()) {
|
||||
if (intraBlockSpendingTxIdSet.contains(input.getSpendingTxId())) {
|
||||
// We have an input from one of the intra-block-transactions, so we cannot process that tx now.
|
||||
// We add the tx for later parsing to the txsWithInputsFromSameBlock and move to the next tx.
|
||||
txsWithInputsFromSameBlock.add(tx);
|
||||
continue outerLoop;
|
||||
}
|
||||
}
|
||||
// If we have not found any tx input pointing to anther tx in the same block we add it to our
|
||||
// txsWithoutInputsFromSameBlock.
|
||||
txsWithoutInputsFromSameBlock.add(tx);
|
||||
}
|
||||
checkArgument(txsWithInputsFromSameBlock.size() + txsWithoutInputsFromSameBlock.size() == transactions.size(),
|
||||
"txsWithInputsFromSameBlock.size + txsWithoutInputsFromSameBlock.size != transactions.size");
|
||||
|
||||
// Usual values is up to 25
|
||||
// There are some blocks where it seems devs have tested graphs of many depending txs, but even
|
||||
// those dont exceed 200 recursions and are mostly old blocks from 2012 when fees have been low ;-).
|
||||
// TODO check strategy btc core uses (sorting the dependency graph would be an optimisation)
|
||||
// Seems btc core delivers tx list sorted by dependency graph. -> TODO verify and test
|
||||
if (recursionCounter > 100) {
|
||||
log.warn("Unusual high recursive calls at resolveConnectedTxs. recursionCounter=" + recursionCounter);
|
||||
log.warn("blockHeight=" + blockHeight);
|
||||
log.warn("txsWithoutInputsFromSameBlock.size " + txsWithoutInputsFromSameBlock.size());
|
||||
log.warn("txsWithInputsFromSameBlock.size " + txsWithInputsFromSameBlock.size());
|
||||
// log.warn("txsWithInputsFromSameBlock " + txsWithInputsFromSameBlock.stream().map(e->e.getId()).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
// we check if we have any valid BSQ from that tx set
|
||||
if (!txsWithoutInputsFromSameBlock.isEmpty()) {
|
||||
for (Tx tx : txsWithoutInputsFromSameBlock) {
|
||||
parseTx(tx, blockHeight, txOutputMap);
|
||||
}
|
||||
log.debug("Parsing of all txsWithoutInputsFromSameBlock is done.");
|
||||
}
|
||||
|
||||
// we check if we have any valid BSQ utxo from that tx set
|
||||
// We might have InputsFromSameBlock which are BTC only but not BSQ, so we cannot
|
||||
// optimize here and need to iterate further.
|
||||
if (!txsWithInputsFromSameBlock.isEmpty()) {
|
||||
if (recursionCounter < maxRecursions) {
|
||||
parseTransactions(txsWithInputsFromSameBlock, txOutputMap, blockHeight,
|
||||
++recursionCounter, maxRecursions);
|
||||
} else {
|
||||
final String msg = "We exceeded our max. recursions for resolveConnectedTxs.\n" +
|
||||
"txsWithInputsFromSameBlock=" + txsWithInputsFromSameBlock.toString() + "\n" +
|
||||
"txsWithoutInputsFromSameBlock=" + txsWithoutInputsFromSameBlock;
|
||||
log.warn(msg);
|
||||
if (DevEnv.DEV_MODE)
|
||||
throw new RuntimeException(msg);
|
||||
}
|
||||
} else {
|
||||
log.debug("We have no more txsWithInputsFromSameBlock.");
|
||||
}
|
||||
}
|
||||
|
||||
private void parseTx(Tx tx,
|
||||
int blockHeight,
|
||||
TxOutputMap txOutputMap) {
|
||||
List<TxOutput> outputs = tx.getOutputs();
|
||||
long availableValue = 0;
|
||||
final String txId = tx.getId();
|
||||
for (int inputIndex = 0; inputIndex < tx.getInputs().size(); inputIndex++) {
|
||||
TxInput input = tx.getInputs().get(inputIndex);
|
||||
final TxOutput txOutputFromSpendingTx = txOutputMap.get(input.getSpendingTxId(), input.getSpendingTxOutputIndex());
|
||||
if (txOutputFromSpendingTx != null &&
|
||||
txOutputFromSpendingTx.isVerified() &&
|
||||
txOutputFromSpendingTx.isUnSpend()) {
|
||||
txOutputFromSpendingTx.setSpendInfo(new SpendInfo(blockHeight, txId, inputIndex));
|
||||
availableValue = availableValue + txOutputFromSpendingTx.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an input spending tokens we iterate the outputs
|
||||
if (availableValue > 0) {
|
||||
// We use order of output index. An output is a BSQ utxo as long there is enough input value
|
||||
for (TxOutput txOutput : outputs) {
|
||||
final long txOutputValue = txOutput.getValue();
|
||||
if (availableValue >= txOutputValue) {
|
||||
// We are spending available tokens
|
||||
txOutput.setVerified(true);
|
||||
txOutputMap.put(txOutput);
|
||||
availableValue -= txOutputValue;
|
||||
if (availableValue == 0) {
|
||||
log.debug("We don't have anymore BSQ to spend");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (availableValue > 0) {
|
||||
log.debug("BSQ have been left which was not spent. Burned BSQ amount={}, tx={}",
|
||||
availableValue,
|
||||
tx.toString());
|
||||
final long finalAvailableValue = availableValue;
|
||||
tx.getOutputs().stream().forEach(e -> e.setBurnedFee(finalAvailableValue));
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getIntraBlockSpendingTxIdSet(List<Tx> transactions) {
|
||||
Set<String> txIdSet = transactions.stream().map(Tx::getId).collect(Collectors.toSet());
|
||||
Set<String> intraBlockSpendingTxIdSet = new HashSet<>();
|
||||
transactions.stream()
|
||||
.forEach(tx -> tx.getInputs().stream()
|
||||
.filter(input -> txIdSet.contains(input.getSpendingTxId()))
|
||||
.forEach(input -> intraBlockSpendingTxIdSet.add(input.getSpendingTxId())));
|
||||
return intraBlockSpendingTxIdSet;
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
/*
|
||||
* 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 io.bisq.common.storage.Storage;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
// Map of any ever existing TxOutput which was a valid BSQ
|
||||
@Slf4j
|
||||
public class BsqTXOMap implements Serializable {
|
||||
// We don't use a Lombok delegate here as we want control the access to our map
|
||||
@Getter
|
||||
private HashMap<TxIdIndexTuple, TxOutput> map = new HashMap<>();
|
||||
@Getter
|
||||
private HashSet<String> txIdSet = new HashSet<>();
|
||||
@Getter
|
||||
private HashMap<String, Tx> burnedBSQTxMap = new HashMap<>();
|
||||
@Getter
|
||||
@Setter
|
||||
private int snapshotHeight;
|
||||
|
||||
private transient ObservableMap<TxIdIndexTuple, TxOutput> observableMap;
|
||||
private transient ObservableMap<String, Tx> observableBurnedBSQTxMap;
|
||||
private transient final Storage<BsqTXOMap> storage;
|
||||
|
||||
public BsqTXOMap(File storageDir) {
|
||||
storage = new Storage<>(storageDir);
|
||||
BsqTXOMap persisted = storage.initAndGetPersisted(this, "BsqTXOMap");
|
||||
if (persisted != null) {
|
||||
map.putAll(persisted.getMap());
|
||||
snapshotHeight = persisted.getSnapshotHeight();
|
||||
txIdSet = persisted.getTxIdSet();
|
||||
}
|
||||
|
||||
observableMap = FXCollections.observableHashMap();
|
||||
observableMap.putAll(map);
|
||||
observableBurnedBSQTxMap = FXCollections.observableHashMap();
|
||||
observableBurnedBSQTxMap.putAll(burnedBSQTxMap);
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
try {
|
||||
in.defaultReadObject();
|
||||
observableMap = FXCollections.observableHashMap();
|
||||
observableMap.putAll(map);
|
||||
observableBurnedBSQTxMap = FXCollections.observableHashMap();
|
||||
observableBurnedBSQTxMap.putAll(burnedBSQTxMap);
|
||||
} catch (Throwable t) {
|
||||
log.warn("Cannot be deserialized." + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Object add(TxOutput txOutput) {
|
||||
txIdSet.add(txOutput.getTxId());
|
||||
final TxOutput result = map.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
|
||||
observableMap.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Object addBurnedBSQTx(Tx tx) {
|
||||
Tx result = burnedBSQTxMap.put(tx.getId(), tx);
|
||||
observableBurnedBSQTxMap.put(tx.getId(), tx);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void persist() {
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
public boolean containsTuple(String txId, int index) {
|
||||
return map.containsKey(new TxIdIndexTuple(txId, index));
|
||||
}
|
||||
|
||||
public TxOutput getByTuple(String txId, int index) {
|
||||
return map.get(new TxIdIndexTuple(txId, index));
|
||||
}
|
||||
|
||||
public void addListener(MapChangeListener<TxIdIndexTuple, TxOutput> listener) {
|
||||
observableMap.addListener(listener);
|
||||
}
|
||||
|
||||
public void addBurnedBSQTxMapListener(MapChangeListener<String, Tx> listener) {
|
||||
observableBurnedBSQTxMap.addListener(listener);
|
||||
}
|
||||
|
||||
public Collection<TxOutput> values() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
public Set<Map.Entry<TxIdIndexTuple, TxOutput>> entrySet() {
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BsqUTXOMap " + map.toString();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
|
||||
public interface BsqTxoListener {
|
||||
void onBsqTxoChanged(BsqTXOMap bsqTXOMap);
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* This file is part of bisq.
|
||||
*
|
||||
* bisq 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.
|
||||
*
|
||||
* bisq 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 bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
// Estimation for UTXO set: 1 UTXO object has 78 byte
|
||||
// 1000 UTXOs - 10 000 UTXOs: 78kb -780kb
|
||||
|
||||
@Value
|
||||
@Slf4j
|
||||
public class BsqUTXO implements Serializable {
|
||||
private final int height;
|
||||
private final TxOutput txOutput;
|
||||
private final boolean isBsqCoinBase;
|
||||
private final String utxoId;
|
||||
|
||||
public BsqUTXO(int height, TxOutput txOutput, boolean isBsqCoinBase) {
|
||||
this.height = height;
|
||||
this.txOutput = txOutput;
|
||||
this.isBsqCoinBase = isBsqCoinBase;
|
||||
|
||||
utxoId = txOutput.getTxId() + ":" + txOutput.getIndex();
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return txOutput.getAddress();
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return txOutput.getValue();
|
||||
}
|
||||
|
||||
public String getTxId() {
|
||||
return txOutput.getTxId();
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return txOutput.getIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BsqUTXO{" +
|
||||
"\n height=" + height +
|
||||
", \n output=" + txOutput +
|
||||
", \n isBsqCoinBase=" + isBsqCoinBase +
|
||||
", \n utxoId='" + utxoId + '\'' +
|
||||
"\n}";
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package io.bisq.core.dao.blockchain;
|
||||
|
||||
|
||||
public interface BsqUTXOListener {
|
||||
void onBsqUTXOChanged(BsqUTXOMap bsqUTXOMap);
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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 io.bisq.common.storage.Storage;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class BsqUTXOMap implements Serializable {
|
||||
// We don't use a Lombok delegate here as we want control the access to our map
|
||||
@Getter
|
||||
private HashMap<TxIdIndexTuple, BsqUTXO> map = new HashMap<>();
|
||||
@Getter
|
||||
private HashSet<String> txIdSet = new HashSet<>();
|
||||
@Getter
|
||||
@Setter
|
||||
private int snapshotHeight;
|
||||
|
||||
private transient ObservableMap<TxIdIndexTuple, BsqUTXO> observableMap;
|
||||
private transient final Storage<BsqUTXOMap> storage;
|
||||
|
||||
public BsqUTXOMap(File storageDir) {
|
||||
storage = new Storage<>(storageDir);
|
||||
BsqUTXOMap persisted = storage.initAndGetPersisted(this, "BsqUTXOMap");
|
||||
if (persisted != null) {
|
||||
map.putAll(persisted.getMap());
|
||||
txIdSet = persisted.getTxIdSet();
|
||||
snapshotHeight = persisted.getSnapshotHeight();
|
||||
}
|
||||
|
||||
observableMap = FXCollections.observableHashMap();
|
||||
observableMap.putAll(map);
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
try {
|
||||
in.defaultReadObject();
|
||||
observableMap = FXCollections.observableHashMap();
|
||||
observableMap.putAll(map);
|
||||
} catch (Throwable t) {
|
||||
log.warn("Cannot be deserialized." + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Object add(BsqUTXO bsqUTXO) {
|
||||
txIdSet.add(bsqUTXO.getTxId());
|
||||
final BsqUTXO result = map.put(new TxIdIndexTuple(bsqUTXO.getTxId(), bsqUTXO.getIndex()), bsqUTXO);
|
||||
observableMap.put(new TxIdIndexTuple(bsqUTXO.getTxId(), bsqUTXO.getIndex()), bsqUTXO);
|
||||
return result;
|
||||
}
|
||||
|
||||
public BsqUTXO removeByTuple(String txId, int index) {
|
||||
txIdSet.remove(txId);
|
||||
final BsqUTXO result = map.remove(new TxIdIndexTuple(txId, index));
|
||||
observableMap.remove(new TxIdIndexTuple(txId, index));
|
||||
return result;
|
||||
}
|
||||
|
||||
public void persist() {
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
public boolean containsTuple(String txId, int index) {
|
||||
return map.containsKey(new TxIdIndexTuple(txId, index));
|
||||
}
|
||||
|
||||
public BsqUTXO getByTuple(String txId, int index) {
|
||||
return observableMap.get(new TxIdIndexTuple(txId, index));
|
||||
}
|
||||
|
||||
public void addListener(MapChangeListener<TxIdIndexTuple, BsqUTXO> listener) {
|
||||
observableMap.addListener(listener);
|
||||
}
|
||||
|
||||
public Collection<BsqUTXO> values() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
public Set<Map.Entry<TxIdIndexTuple, BsqUTXO>> entrySet() {
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BsqUTXOMap " + map.toString();
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,6 @@ public class TxOutput implements Serializable {
|
||||
@Nullable
|
||||
private String address;
|
||||
|
||||
|
||||
public TxOutput(int index,
|
||||
long value,
|
||||
String txId,
|
||||
@ -105,16 +104,35 @@ public class TxOutput implements Serializable {
|
||||
return address;
|
||||
}
|
||||
|
||||
public boolean isUnSpend() {
|
||||
return spendInfo == null;
|
||||
}
|
||||
|
||||
public boolean hasBurnedFee() {
|
||||
return burnedFee > 0;
|
||||
}
|
||||
|
||||
public String getTxoId() {
|
||||
return txId + ":" + index;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TxOutput{" +
|
||||
"\n index=" + index +
|
||||
",\n value=" + value +
|
||||
",\n address=" + getAddress() +
|
||||
",\n txId=" + txId +
|
||||
",\n pubKeyScript=" + pubKeyScript +
|
||||
",\n spendInfo=" + spendInfo +
|
||||
"\n" +
|
||||
" }";
|
||||
"\n index=" + index +
|
||||
",\n value=" + value +
|
||||
",\n txId='" + txId + '\'' +
|
||||
",\n pubKeyScript=" + pubKeyScript +
|
||||
",\n blockHeight=" + blockHeight +
|
||||
",\n time=" + time +
|
||||
",\n txVersion='" + txVersion + '\'' +
|
||||
",\n isBsqCoinBase=" + isBsqCoinBase +
|
||||
",\n isVerified=" + isVerified +
|
||||
",\n burnedFee=" + burnedFee +
|
||||
",\n btcTxFee=" + btcTxFee +
|
||||
",\n spendInfo=" + spendInfo +
|
||||
",\n address='" + getAddress() + '\'' +
|
||||
"\n}";
|
||||
}
|
||||
}
|
||||
|
134
core/src/main/java/io/bisq/core/dao/blockchain/TxOutputMap.java
Normal file
134
core/src/main/java/io/bisq/core/dao/blockchain/TxOutputMap.java
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 io.bisq.common.storage.Storage;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
// Map of any ever existing TxOutput which was a valid BSQ
|
||||
@Slf4j
|
||||
public class TxOutputMap implements Serializable {
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Interface
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public interface Listener {
|
||||
void onMapChanged(TxOutputMap txOutputMap);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Instance fields
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// We don't use a Lombok delegate here as we want control the access to our map
|
||||
@Getter
|
||||
private HashMap<TxIdIndexTuple, TxOutput> map = new HashMap<>();
|
||||
@Getter
|
||||
@Setter
|
||||
private int snapshotHeight = 0;
|
||||
|
||||
private transient final Storage<TxOutputMap> storage;
|
||||
private final List<Listener> listeners = new ArrayList<>();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public TxOutputMap(File storageDir) {
|
||||
storage = new Storage<>(storageDir);
|
||||
TxOutputMap persisted = storage.initAndGetPersisted(this, "BsqTxOutputMap");
|
||||
if (persisted != null) {
|
||||
map.putAll(persisted.getMap());
|
||||
snapshotHeight = persisted.getSnapshotHeight();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Public methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public boolean isTxOutputUnSpent(String txId, int index) {
|
||||
final TxOutput txOutput = get(txId, index);
|
||||
return txOutput != null && txOutput.isUnSpend();
|
||||
}
|
||||
|
||||
public boolean hasTxBurnedFee(String txId) {
|
||||
final TxOutput txOutput = get(txId, 0);
|
||||
return txOutput != null && txOutput.hasBurnedFee();
|
||||
}
|
||||
|
||||
public void persist() {
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Delegated map methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Object put(TxOutput txOutput) {
|
||||
final TxOutput result = map.put(new TxIdIndexTuple(txOutput.getTxId(), txOutput.getIndex()), txOutput);
|
||||
listeners.stream().forEach(l -> l.onMapChanged(this));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TxOutput get(String txId, int index) {
|
||||
return map.get(new TxIdIndexTuple(txId, index));
|
||||
}
|
||||
|
||||
public boolean contains(String txId, int index) {
|
||||
return map.containsKey(new TxIdIndexTuple(txId, index));
|
||||
}
|
||||
|
||||
public Collection<TxOutput> values() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BsqTxOutputMap " + map.toString();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* This file is part of bisq.
|
||||
*
|
||||
* bisq 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.
|
||||
*
|
||||
* bisq 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 bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bisq.core.dao.blockchain.json;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.neemre.btcdcli4j.core.domain.PubKeyScript;
|
||||
import io.bisq.common.storage.PlainTextWrapper;
|
||||
import io.bisq.common.storage.Storage;
|
||||
import io.bisq.common.util.Utilities;
|
||||
import io.bisq.core.dao.RpcOptionKeys;
|
||||
import io.bisq.core.dao.blockchain.SpendInfo;
|
||||
import io.bisq.core.dao.blockchain.TxOutput;
|
||||
import io.bisq.core.dao.blockchain.TxOutputMap;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JsonExporter {
|
||||
private final Storage<PlainTextWrapper> jsonStorage;
|
||||
private boolean dumpBlockchainData;
|
||||
private final File storageDir;
|
||||
|
||||
@Inject
|
||||
public JsonExporter(Storage<PlainTextWrapper> jsonStorage,
|
||||
@Named(Storage.DIR_KEY) File storageDir,
|
||||
@Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
|
||||
this.storageDir = storageDir;
|
||||
this.jsonStorage = jsonStorage;
|
||||
this.dumpBlockchainData = dumpBlockchainData;
|
||||
}
|
||||
|
||||
public void init(@Named(RpcOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) {
|
||||
if (dumpBlockchainData) {
|
||||
this.jsonStorage.initWithFileName("txo.json");
|
||||
}
|
||||
}
|
||||
|
||||
public void export(TxOutputMap txOutputMap) {
|
||||
if (dumpBlockchainData) {
|
||||
List<TxOutputForJson> list = txOutputMap.getMap().values().stream()
|
||||
.map(this::getTxOutputForJson)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
list.sort((o1, o2) -> (o1.getSortData().compareTo(o2.getSortData())));
|
||||
TxOutputForJson[] array = new TxOutputForJson[list.size()];
|
||||
list.toArray(array);
|
||||
jsonStorage.queueUpForSave(new PlainTextWrapper(Utilities.objectToJson(array)), 5000);
|
||||
|
||||
// keep the individual file storage option as code as we dont know yet what we will use.
|
||||
/* log.error("txOutputForJson " + txOutputForJson);
|
||||
File txoDir = new File(Paths.get(storageDir.getAbsolutePath(), "txo").toString());
|
||||
if (!txoDir.exists())
|
||||
if (!txoDir.mkdir())
|
||||
log.warn("make txoDir failed.\ntxoDir=" + txoDir.getAbsolutePath());
|
||||
File txoFile = new File(Paths.get(txoDir.getAbsolutePath(),
|
||||
txOutput.getTxId() + ":" + outputIndex + ".json").toString());
|
||||
|
||||
// Nr of write requests might be a bit heavy, consider write whole list to one file
|
||||
FileManager<PlainTextWrapper> fileManager = new FileManager<>(storageDir, txoFile, 1);
|
||||
fileManager.saveLater(new PlainTextWrapper(Utilities.objectToJson(txOutputForJson)));*/
|
||||
}
|
||||
}
|
||||
|
||||
private TxOutputForJson getTxOutputForJson(TxOutput txOutput) {
|
||||
String txId = txOutput.getTxId();
|
||||
int outputIndex = txOutput.getIndex();
|
||||
final long bsqAmount = txOutput.getValue();
|
||||
final int height = txOutput.getBlockHeight();
|
||||
final boolean isBsqCoinBase = txOutput.isBsqCoinBase();
|
||||
final boolean verified = txOutput.isVerified();
|
||||
final long burnedFee = txOutput.getBurnedFee();
|
||||
final long btcTxFee = txOutput.getBtcTxFee();
|
||||
|
||||
PubKeyScript pubKeyScript = txOutput.getPubKeyScript();
|
||||
final ScriptPubKeyForJson scriptPubKey = new ScriptPubKeyForJson(pubKeyScript.getAddresses(),
|
||||
pubKeyScript.getAsm(),
|
||||
pubKeyScript.getHex(),
|
||||
pubKeyScript.getReqSigs(),
|
||||
pubKeyScript.getType().toString());
|
||||
SpentInfoForJson spentInfoJson = null;
|
||||
SpendInfo spendInfo = txOutput.getSpendInfo();
|
||||
if (spendInfo != null)
|
||||
spentInfoJson = new SpentInfoForJson(spendInfo.getBlockHeight(),
|
||||
spendInfo.getInputIndex(),
|
||||
spendInfo.getTxId());
|
||||
|
||||
final long time = txOutput.getTime();
|
||||
final String txVersion = txOutput.getTxVersion();
|
||||
return new TxOutputForJson(txId,
|
||||
outputIndex,
|
||||
bsqAmount,
|
||||
height,
|
||||
isBsqCoinBase,
|
||||
verified,
|
||||
burnedFee,
|
||||
btcTxFee,
|
||||
scriptPubKey,
|
||||
spentInfoJson,
|
||||
time,
|
||||
txVersion
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -31,13 +31,8 @@ import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/*
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
*/
|
||||
@Slf4j
|
||||
public class BsqBlockchainServiceTest {
|
||||
|
||||
@ -64,16 +59,14 @@ 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;
|
||||
private TxOutputMap txOutputMap;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
final URL resource = this.getClass().getClassLoader().getResource("");
|
||||
final String path = resource != null ? resource.getFile() : "";
|
||||
log.info("path for BsqUTXOMap=" + path);
|
||||
bsqUTXOMap = new BsqUTXOMap(new File(path));
|
||||
bsqTXOMap = new BsqTXOMap(new File(path));
|
||||
txOutputMap = new TxOutputMap(new File(path));
|
||||
service = new MockBsqBlockchainService();
|
||||
}
|
||||
|
||||
@ -98,13 +91,13 @@ public class BsqBlockchainServiceTest {
|
||||
|
||||
parseAllBlocksFromGenesis();
|
||||
|
||||
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
|
||||
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(GEN_TX_ID, 1);
|
||||
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(GEN_TX_ID, 1));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
|
||||
assertEquals(ADDRESS_GEN_2_VALUE, bsqUTXO2.getValue());
|
||||
assertEquals(2, bsqUTXOMap.size());
|
||||
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
|
||||
TxOutput bsqTxo2 = txOutputMap.get(GEN_TX_ID, 1);
|
||||
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqTxo2.getTxoId(), getTxoId(GEN_TX_ID, 1));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
|
||||
assertEquals(ADDRESS_GEN_2_VALUE, bsqTxo2.getValue());
|
||||
assertEquals(2, txOutputMap.size());
|
||||
}
|
||||
|
||||
|
||||
@ -137,13 +130,15 @@ public class BsqBlockchainServiceTest {
|
||||
|
||||
parseAllBlocksFromGenesis();
|
||||
|
||||
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
|
||||
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX1_ID, 0);
|
||||
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(TX1_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
|
||||
assertEquals(ADDRESS_TX_1_VALUE, bsqUTXO2.getValue());
|
||||
assertEquals(2, bsqUTXOMap.size());
|
||||
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
|
||||
TxOutput bsqTxo2 = txOutputMap.get(TX1_ID, 0);
|
||||
assertTrue(bsqTxo1.isUnSpend());
|
||||
assertTrue(bsqTxo2.isUnSpend());
|
||||
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqTxo2.getTxoId(), getTxoId(TX1_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
|
||||
assertEquals(ADDRESS_TX_1_VALUE, bsqTxo2.getValue());
|
||||
assertEquals(3, txOutputMap.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -189,13 +184,20 @@ public class BsqBlockchainServiceTest {
|
||||
|
||||
parseAllBlocksFromGenesis();
|
||||
|
||||
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
|
||||
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX2_ID, 0);
|
||||
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(TX2_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
|
||||
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue());
|
||||
assertEquals(2, bsqUTXOMap.size());
|
||||
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
|
||||
TxOutput bsqTxo2 = txOutputMap.get(TX2_ID, 0);
|
||||
|
||||
txOutputMap.values().forEach(e -> {
|
||||
if (e.equals(bsqTxo1) || e.equals(bsqTxo2))
|
||||
assertTrue(e.isUnSpend());
|
||||
else
|
||||
assertFalse(e.isUnSpend());
|
||||
});
|
||||
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqTxo2.getTxoId(), getTxoId(TX2_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
|
||||
assertEquals(ADDRESS_TX_2_VALUE, bsqTxo2.getValue());
|
||||
assertEquals(4, txOutputMap.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -241,13 +243,19 @@ public class BsqBlockchainServiceTest {
|
||||
|
||||
parseAllBlocksFromGenesis();
|
||||
|
||||
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(GEN_TX_ID, 0);
|
||||
BsqUTXO bsqUTXO2 = bsqUTXOMap.getByTuple(TX2_ID, 0);
|
||||
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqUTXO2.getUtxoId(), getUTXOId(TX2_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
|
||||
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue());
|
||||
assertEquals(2, bsqUTXOMap.size());
|
||||
TxOutput bsqTxo1 = txOutputMap.get(GEN_TX_ID, 0);
|
||||
TxOutput bsqTxo2 = txOutputMap.get(TX2_ID, 0);
|
||||
txOutputMap.values().forEach(e -> {
|
||||
if (e.equals(bsqTxo1) || e.equals(bsqTxo2))
|
||||
assertTrue(e.isUnSpend());
|
||||
else
|
||||
assertFalse(e.isUnSpend());
|
||||
});
|
||||
assertEquals(bsqTxo1.getTxoId(), getTxoId(GEN_TX_ID, 0));
|
||||
assertEquals(bsqTxo2.getTxoId(), getTxoId(TX2_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE, bsqTxo1.getValue());
|
||||
assertEquals(ADDRESS_TX_2_VALUE, bsqTxo2.getValue());
|
||||
assertEquals(4, txOutputMap.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -295,10 +303,16 @@ public class BsqBlockchainServiceTest {
|
||||
|
||||
parseAllBlocksFromGenesis();
|
||||
|
||||
BsqUTXO bsqUTXO1 = bsqUTXOMap.getByTuple(TX2_ID, 0);
|
||||
assertEquals(bsqUTXO1.getUtxoId(), getUTXOId(TX2_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE + ADDRESS_GEN_2_VALUE, bsqUTXO1.getValue());
|
||||
assertEquals(1, bsqUTXOMap.size());
|
||||
TxOutput bsqTxo1 = txOutputMap.get(TX2_ID, 0);
|
||||
txOutputMap.values().forEach(e -> {
|
||||
if (e.equals(bsqTxo1))
|
||||
assertTrue(e.isUnSpend());
|
||||
else
|
||||
assertFalse(e.isUnSpend());
|
||||
});
|
||||
assertEquals(bsqTxo1.getTxoId(), getTxoId(TX2_ID, 0));
|
||||
assertEquals(ADDRESS_GEN_1_VALUE + ADDRESS_GEN_2_VALUE, bsqTxo1.getValue());
|
||||
assertEquals(4, txOutputMap.size());
|
||||
}
|
||||
|
||||
|
||||
@ -388,16 +402,16 @@ public class BsqBlockchainServiceTest {
|
||||
|
||||
private void parseAllBlocksFromGenesis()
|
||||
throws BitcoindException, CommunicationException, BsqBlockchainException {
|
||||
service.parseBlockchain(bsqUTXOMap,
|
||||
bsqTXOMap,
|
||||
BsqParser bsqParser = new BsqParser(service);
|
||||
bsqParser.parseBlocks(BLOCK_0,
|
||||
service.requestChainHeadHeight(),
|
||||
BLOCK_0,
|
||||
BLOCK_0,
|
||||
GEN_TX_ID);
|
||||
GEN_TX_ID,
|
||||
txOutputMap);
|
||||
}
|
||||
|
||||
private String getUTXOId(String TxId, int index) {
|
||||
return TxId + ":" + index;
|
||||
private String getTxoId(String txId, int index) {
|
||||
return txId + ":" + index;
|
||||
}
|
||||
}
|
||||
|
||||
@ -416,7 +430,7 @@ class MockBsqBlockchainService extends BsqBlockchainRpcService {
|
||||
private final Map<Integer, List<String>> txIdsInBlockMap = new HashMap<>();
|
||||
|
||||
public MockBsqBlockchainService() {
|
||||
super(null, null, null, null, null);
|
||||
super(null, null, null, null);
|
||||
}
|
||||
|
||||
public void buildBlocks(int from, int to) {
|
||||
@ -489,7 +503,7 @@ class MockBsqBlockchainService extends BsqBlockchainRpcService {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RawTransaction getRawTransaction(String txId) throws BitcoindException, CommunicationException {
|
||||
RawTransaction requestRawTransaction(String txId) throws BitcoindException, CommunicationException {
|
||||
return txByIdMap.get(txId);
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ package io.bisq.gui.main.dao.wallet;
|
||||
|
||||
import io.bisq.core.btc.wallet.BsqBalanceListener;
|
||||
import io.bisq.core.btc.wallet.BsqWalletService;
|
||||
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
|
||||
import io.bisq.core.dao.blockchain.BsqUTXOListener;
|
||||
import io.bisq.gui.util.BsqFormatter;
|
||||
import javafx.scene.control.TextField;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -32,15 +30,13 @@ import javax.inject.Inject;
|
||||
public class BalanceUtil implements BsqBalanceListener {
|
||||
private final BsqWalletService bsqWalletService;
|
||||
private final BsqFormatter formatter;
|
||||
private BsqBlockchainManager bsqBlockchainManager;
|
||||
private TextField balanceTextField;
|
||||
private BsqUTXOListener bsqUTXOListener;
|
||||
|
||||
@Inject
|
||||
private BalanceUtil(BsqWalletService bsqWalletService, BsqFormatter formatter, BsqBlockchainManager bsqBlockchainManager) {
|
||||
private BalanceUtil(BsqWalletService bsqWalletService,
|
||||
BsqFormatter formatter) {
|
||||
this.bsqWalletService = bsqWalletService;
|
||||
this.formatter = formatter;
|
||||
this.bsqBlockchainManager = bsqBlockchainManager;
|
||||
}
|
||||
|
||||
public void setBalanceTextField(TextField balanceTextField) {
|
||||
@ -49,18 +45,15 @@ public class BalanceUtil implements BsqBalanceListener {
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
bsqUTXOListener = bsqUTXOMap -> updateAvailableBalance(bsqWalletService.getAvailableBalance());
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
updateAvailableBalance(bsqWalletService.getAvailableBalance());
|
||||
bsqWalletService.addBsqBalanceListener(this);
|
||||
bsqBlockchainManager.addUtxoListener(bsqUTXOListener);
|
||||
}
|
||||
|
||||
public void deactivate() {
|
||||
bsqWalletService.removeBsqBalanceListener(this);
|
||||
bsqBlockchainManager.removeUtxoListener(bsqUTXOListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -22,7 +22,6 @@ 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.dao.blockchain.Tx;
|
||||
import io.bisq.core.user.DontShowAgainLookup;
|
||||
import io.bisq.core.user.Preferences;
|
||||
import io.bisq.gui.common.view.ActivatableView;
|
||||
@ -46,7 +45,8 @@ import javafx.util.Callback;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -112,7 +112,7 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
|
||||
@Override
|
||||
protected void activate() {
|
||||
balanceUtil.activate();
|
||||
bsqWalletService.getWalletBsqTransactions().addListener(walletBsqTransactionsListener);
|
||||
bsqWalletService.getWalletTransactions().addListener(walletBsqTransactionsListener);
|
||||
|
||||
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
|
||||
tableView.setItems(sortedList);
|
||||
@ -124,25 +124,30 @@ public class BsqTxView extends ActivatableView<GridPane, Void> {
|
||||
protected void deactivate() {
|
||||
balanceUtil.deactivate();
|
||||
sortedList.comparatorProperty().unbind();
|
||||
bsqWalletService.getWalletBsqTransactions().removeListener(walletBsqTransactionsListener);
|
||||
bsqWalletService.getWalletTransactions().removeListener(walletBsqTransactionsListener);
|
||||
observableList.forEach(BsqTxListItem::cleanup);
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
observableList.forEach(BsqTxListItem::cleanup);
|
||||
|
||||
Map<String, Tx> burnedBSQTxIdMap = bsqBlockchainManager.getBsqTXOMap().getBurnedBSQTxMap();
|
||||
Set<BsqTxListItem> list = bsqWalletService.getWalletBsqTransactions().stream()
|
||||
.map(transaction -> new BsqTxListItem(transaction,
|
||||
bsqWalletService,
|
||||
btcWalletService,
|
||||
burnedBSQTxIdMap.containsKey(transaction.getHashAsString()), bsqFormatter)
|
||||
// clone to avoid ConcurrentModificationException
|
||||
final List<Transaction> walletTransactions = new ArrayList<>(bsqWalletService.getWalletTransactions());
|
||||
Set<BsqTxListItem> list = walletTransactions.stream()
|
||||
.map(transaction -> {
|
||||
// The burned fee is added to all outputs of a tx, so we just ask at index 0
|
||||
return new BsqTxListItem(transaction,
|
||||
bsqWalletService,
|
||||
btcWalletService,
|
||||
bsqBlockchainManager.getTxOutputMap().hasTxBurnedFee(transaction.getHashAsString()),
|
||||
bsqFormatter);
|
||||
}
|
||||
)
|
||||
.collect(Collectors.toSet());
|
||||
observableList.setAll(list);
|
||||
|
||||
final Set<Transaction> invalidBsqTransactions = bsqWalletService.getInvalidBsqTransactions();
|
||||
if (!invalidBsqTransactions.isEmpty() && bsqBlockchainManager.isUtxoSyncWithChainHeadHeight()) {
|
||||
if (!invalidBsqTransactions.isEmpty() && bsqBlockchainManager.isParseBlockchainComplete()) {
|
||||
Set<String> txIds = invalidBsqTransactions.stream()
|
||||
.filter(t -> t != null)
|
||||
.map(t -> t.getHashAsString()).collect(Collectors.toSet());
|
||||
|
Loading…
Reference in New Issue
Block a user