Fix BSQ algo, add tx view

This commit is contained in:
Manfred Karrer 2017-04-02 20:19:03 -05:00
parent db966ca01c
commit 0386f1e739
31 changed files with 951 additions and 322 deletions

View file

@ -610,6 +610,7 @@ funds.locked.locked=Locked in MultiSig for trade with ID: {0}
funds.tx.direction.sentTo=Sent to:
funds.tx.direction.receivedWith=Received with:
funds.tx.txFeePaymentForBsqTx=Tx fee payment for BSQ tx
funds.tx.createOfferFee=Maker fee: {0}
funds.tx.takeOfferFee=Taker fee: {0}
funds.tx.multiSigDeposit=MultiSig deposit: {0}
@ -1514,6 +1515,7 @@ validation.bic.letters=Bank and Country code must be letters
validation.bic.invalidLocationCode=BIC contains invalid location code
validation.bic.invalidBranchCode=BIC contains invalid branch code
validation.btc.invalidFormat=Invalid format of the bitcoin address.
validation.bsq.invalidFormat=Invalid format of the BSQ address.
validation.email.invalidAddress=Invalid address=
validation.iban.invalidCountryCode=Country code invalid
validation.iban.checkSumNotNumeric=Checksum must be numeric

View file

@ -19,21 +19,25 @@ package io.bisq.core.btc.wallet;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.handlers.ResultHandler;
import io.bisq.core.btc.Restrictions;
import io.bisq.core.btc.exceptions.TransactionVerificationException;
import io.bisq.core.btc.exceptions.WalletException;
import io.bisq.core.dao.blockchain.BsqBlock;
import io.bisq.core.dao.blockchain.BsqBlockchainManager;
import io.bisq.core.dao.blockchain.BsqUTXOMap;
import io.bisq.core.dao.blockchain.Tx;
import io.bisq.core.provider.fee.FeeService;
import io.bisq.core.user.Preferences;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.wallet.CoinSelection;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.List;
@ -42,11 +46,13 @@ import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class BsqWalletService extends WalletService {
private static final Logger log = LoggerFactory.getLogger(BsqWalletService.class);
private final BsqBlockchainManager bsqBlockchainManager;
private final BsqCoinSelector bsqCoinSelector;
@Getter
private final ObservableList<Transaction> walletBsqTransactions = FXCollections.observableArrayList();
///////////////////////////////////////////////////////////////////////////////////////////
@ -73,44 +79,41 @@ public class BsqWalletService extends WalletService {
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
onChange();
//TODO do we need updateWalletBsqTransactions(); here?
}
@Override
public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
onChange();
//TODO do we need updateWalletBsqTransactions(); here?
}
@Override
public void onReorganize(Wallet wallet) {
onChange();
updateWalletBsqTransactions();
}
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
onChange();
}
@Override
public void onKeysAdded(List<ECKey> keys) {
onChange();
updateWalletBsqTransactions();
}
@Override
public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {
onChange();
updateWalletBsqTransactions();
}
@Override
public void onWalletChanged(Wallet wallet) {
onChange();
updateWalletBsqTransactions();
}
public void onChange() {
// TODO
}
});
});
bsqBlockchainManager.getBsqBlocks().addListener((ListChangeListener<BsqBlock>) c -> updateWalletBsqTransactions());
}
@ -141,7 +144,7 @@ public class BsqWalletService extends WalletService {
// UTXO
///////////////////////////////////////////////////////////////////////////////////////////
public void requestBsqUtxo(@Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) {
public void requestBsqUtxo(@Nullable ResultHandler resultHandler) {
if (bsqBlockchainManager.isUtxoAvailable()) {
applyUtxoSetToUTXOProvider(bsqBlockchainManager.getUtxoByTxIdMap());
if (resultHandler != null)
@ -159,6 +162,17 @@ public class BsqWalletService extends WalletService {
bsqCoinSelector.setUtxoMap(bsqUTXOMap);
}
private void updateWalletBsqTransactions() {
Set<String> txIdsFromUTXOSet = bsqBlockchainManager.getBsqBlocks().stream()
.flatMap(bsqBlock -> bsqBlock.getTxByTxIdMap().values().stream())
.map(Tx::getId)
.collect(Collectors.toSet());
walletBsqTransactions.setAll(getTransactions(true).stream()
.filter(t -> txIdsFromUTXOSet.contains(t.getHashAsString()))
.collect(Collectors.toSet()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Sign tx

View file

@ -27,6 +27,7 @@ import io.bisq.core.btc.exceptions.WalletException;
import io.bisq.core.btc.listeners.AddressConfidenceListener;
import io.bisq.core.btc.listeners.BalanceListener;
import io.bisq.core.btc.listeners.TxConfidenceListener;
import io.bisq.core.dao.blockchain.TxOutput;
import io.bisq.core.provider.fee.FeeService;
import io.bisq.core.user.Preferences;
import org.bitcoinj.core.*;
@ -319,6 +320,7 @@ public abstract class WalletService {
@Nullable
public TransactionConfidence getConfidenceForTxId(String txId) {
if (wallet != null) {
// TODO includeDead txs?
Set<Transaction> transactions = wallet.getTransactions(true);
for (Transaction tx : transactions) {
if (tx.getHashAsString().equals(txId))
@ -543,6 +545,26 @@ public abstract class WalletService {
return transactionOutput.isMine(wallet);
}
public boolean isTxOutputMine(TxOutput txOutput) {
try {
Script script = txOutput.getScript();
if (script.isSentToRawPubKey()) {
byte[] pubkey = script.getPubKey();
return wallet.isPubKeyMine(pubkey);
}
if (script.isPayToScriptHash()) {
return wallet.isPayToScriptHashMine(script.getPubKeyHash());
} else {
byte[] pubkeyHash = script.getPubKeyHash();
return wallet.isPubKeyHashMine(pubkeyHash);
}
} catch (ScriptException e) {
// Just means we didn't understand the output of this transaction: ignore it.
log.debug("Could not parse tx output script: {}", e.toString());
return false;
}
}
public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException {
return transaction.getValueSentFromMe(wallet);
}
@ -589,7 +611,6 @@ public abstract class WalletService {
TransactionConfidence transactionConfidence = getMostRecentConfidence(transactionConfidenceList);
addressConfidenceListener.onTransactionConfidenceChanged(transactionConfidence);
}
txConfidenceListeners.stream()
.filter(txConfidenceListener -> tx != null &&
tx.getHashAsString() != null &&

View file

@ -18,14 +18,12 @@
package io.bisq.core.dao.blockchain;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Value
@Slf4j
public class BsqBlock {
private final int height;
private final List<String> txIds;
@ -43,4 +41,13 @@ public class BsqBlock {
public Tx getTxByTxId(String txId) {
return txByTxIdMap.get(txId);
}
@Override
public String toString() {
return "BsqBlock{" +
"\nheight=" + height +
",\ntxIds=" + txIds +
",\ntxByTxIdMap=" + txByTxIdMap +
"}\n";
}
}

View file

@ -23,13 +23,17 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import io.bisq.common.UserThread;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.util.Tuple2;
import io.bisq.common.util.Tuple3;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class BsqBlockchainManager {
private static final Logger log = LoggerFactory.getLogger(BsqBlockchainManager.class);
@ -37,13 +41,18 @@ public class BsqBlockchainManager {
private final BsqBlockchainService blockchainService;
// regtest
public static final String GENESIS_TX_ID = "c0ddf75202579fd43e0d5a41ac5bae05a6e8295633695af6c350b9100878c5e7";
public static final String GENESIS_TX_ID = "3bc7bc9484e112ec8ddd1a1c984379819245ac463b9ce40fa8b5bf771c0f9236";
public static final int GENESIS_BLOCK_HEIGHT = 102;
protected BsqUTXOMap utxoByTxIdMap;
private BsqUTXOMap utxoByTxIdMap;
private final List<BsqUTXOListener> bsqUTXOListeners = new ArrayList<>();
private boolean isUtxoAvailable;
protected int chainHeadHeight;
@Getter
private int chainHeadHeight;
// We prefer a list over a set. See: http://stackoverflow.com/questions/24799125/what-is-the-observableset-equivalent-for-setall-method-from-observablelist
@Getter
private final ObservableList<BsqBlock> bsqBlocks = FXCollections.observableArrayList();
///////////////////////////////////////////////////////////////////////////////////////////
@ -83,18 +92,19 @@ public class BsqBlockchainManager {
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
// private methods
///////////////////////////////////////////////////////////////////////////////////////////
protected void setupComplete() {
ListenableFuture<Tuple2<BsqUTXOMap, Integer>> future =
private void setupComplete() {
ListenableFuture<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>> future =
blockchainService.syncFromGenesis(GENESIS_BLOCK_HEIGHT, GENESIS_TX_ID);
Futures.addCallback(future, new FutureCallback<Tuple2<BsqUTXOMap, Integer>>() {
Futures.addCallback(future, new FutureCallback<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>>() {
@Override
public void onSuccess(Tuple2<BsqUTXOMap, Integer> tuple) {
public void onSuccess(Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer> tuple) {
UserThread.execute(() -> {
BsqBlockchainManager.this.utxoByTxIdMap = tuple.first;
chainHeadHeight = tuple.second;
BsqBlockchainManager.this.bsqBlocks.setAll(tuple.first);
BsqBlockchainManager.this.utxoByTxIdMap = tuple.second;
chainHeadHeight = tuple.third;
isUtxoAvailable = true;
bsqUTXOListeners.stream().forEach(e -> e.onBsqUTXOChanged(utxoByTxIdMap));
blockchainService.syncFromGenesisCompete(GENESIS_TX_ID,
@ -103,15 +113,17 @@ public class BsqBlockchainManager {
if (btcdBlock != null) {
UserThread.execute(() -> {
try {
blockchainService.parseBlock(new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight()),
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
blockchainService.parseBlock(bsqBlock,
GENESIS_BLOCK_HEIGHT,
GENESIS_TX_ID,
utxoByTxIdMap);
if (!BsqBlockchainManager.this.bsqBlocks.contains(bsqBlock))
BsqBlockchainManager.this.bsqBlocks.add(bsqBlock);
} catch (BsqBlockchainException e) {
//TODO
e.printStackTrace();
}
blockchainService.printUtxoMap(utxoByTxIdMap);
});
}
});
@ -124,25 +136,4 @@ public class BsqBlockchainManager {
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public int getChainHeadHeight() {
return chainHeadHeight;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
}

View file

@ -33,12 +33,12 @@ import io.bisq.common.UserThread;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.handlers.ResultHandler;
import io.bisq.common.util.Tuple2;
import io.bisq.common.util.Tuple3;
import io.bisq.common.util.Utilities;
import io.bisq.core.dao.RpcOptionKeys;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.script.Script;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@ -47,7 +47,9 @@ import org.slf4j.LoggerFactory;
import javax.inject.Named;
import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -137,13 +139,13 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
}
@Override
protected ListenableFuture<Tuple2<BsqUTXOMap, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId) {
protected ListenableFuture<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId) {
return rpcRequestsExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
int chainHeadHeight = requestChainHeadHeight();
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis(chainHeadHeight, genesisBlockHeight, genesisTxId);
Tuple2<Set<BsqBlock>, BsqUTXOMap> tuple2 = parseAllBlocksFromGenesis(chainHeadHeight, genesisBlockHeight, genesisTxId);
log.info("syncFromGenesis took {} ms", System.currentTimeMillis() - startTs);
return new Tuple2<>(bsqUTXOMap, chainHeadHeight);
return new Tuple3<>(tuple2.first, tuple2.second, chainHeadHeight);
});
}
@ -186,20 +188,25 @@ public class BsqBlockchainRpcService extends BsqBlockchainService {
Tx requestTransaction(String txId) throws BsqBlockchainException {
try {
RawTransaction rawTransaction = getRawTransaction(txId);
final List<TxInput> txInputs = rawTransaction.getVIn()
.stream()
.filter(rawInput -> rawInput != null && rawInput.getVOut() != null && rawInput.getTxId() != null)
.map(rawInput -> new TxInput(rawInput.getVOut(), rawInput.getTxId(), rawTransaction.getHex()))
.collect(Collectors.toList());
final List<TxOutput> txOutputs = rawTransaction.getVOut()
.stream()
.filter(e -> e != null && e.getN() != null && e.getValue() != null && e.getScriptPubKey() != null)
.map(e -> new TxOutput(e.getN(),
e.getValue().movePointRight(8).longValue(),
e.getScriptPubKey().getAddresses(),
e.getScriptPubKey().getHex() != null ? new Script(HEX.decode(e.getScriptPubKey().getHex())) : null))
.collect(Collectors.toList());
// rawTransaction.getTime() is in seconds but we keep it in ms internally
return new Tx(txId,
rawTransaction.getVIn()
.stream()
.filter(rawInput -> rawInput != null && rawInput.getVOut() != null && rawInput.getTxId() != null)
.map(rawInput -> new TxInput(rawInput.getVOut(), rawInput.getTxId(), rawTransaction.getHex()))
.collect(Collectors.toList()),
rawTransaction.getVOut()
.stream()
.filter(e -> e != null && e.getN() != null && e.getValue() != null && e.getScriptPubKey() != null)
.map(e -> new TxOutput(e.getN(),
Coin.valueOf(e.getValue().movePointRight(8).longValue()),
e.getScriptPubKey().getAddresses(),
e.getScriptPubKey().getHex() != null ? new Script(HEX.decode(e.getScriptPubKey().getHex())) : null))
.collect(Collectors.toList()));
txInputs,
txOutputs,
rawTransaction.getTime() * 1000);
} catch (BitcoindException | CommunicationException e) {
throw new BsqBlockchainException(e.getMessage(), e);
}

View file

@ -27,7 +27,7 @@ import io.bisq.common.app.DevEnv;
import io.bisq.common.handlers.ErrorMessageHandler;
import io.bisq.common.handlers.ResultHandler;
import io.bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import io.bisq.common.util.Tuple3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -56,7 +56,7 @@ abstract public class BsqBlockchainService {
abstract void setup(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler);
abstract ListenableFuture<Tuple2<BsqUTXOMap, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId);
abstract ListenableFuture<Tuple3<Set<BsqBlock>, BsqUTXOMap, Integer>> syncFromGenesis(int genesisBlockHeight, String genesisTxId);
abstract void syncFromGenesisCompete(String genesisTxId, int genesisBlockHeight, Consumer<Block> onNewBlockHandler);
@ -67,24 +67,27 @@ abstract public class BsqBlockchainService {
abstract Tx requestTransaction(String txId) throws BsqBlockchainException;
@VisibleForTesting
BsqUTXOMap parseAllBlocksFromGenesis(int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId) throws BsqBlockchainException {
Tuple2<Set<BsqBlock>, BsqUTXOMap> parseAllBlocksFromGenesis(int chainHeadHeight,
int genesisBlockHeight,
String genesisTxId) throws BsqBlockchainException {
try {
BsqUTXOMap bsqUTXOMap = new BsqUTXOMap();
Set<BsqBlock> blocks = new HashSet<>();
log.info("chainHeadHeight=" + chainHeadHeight);
long startTs = System.currentTimeMillis();
for (int height = genesisBlockHeight; height <= chainHeadHeight; height++) {
Block btcdBlock = requestBlock(height);
log.info("Current block height=" + height);
parseBlock(new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight()),
final BsqBlock bsqBlock = new BsqBlock(btcdBlock.getTx(), btcdBlock.getHeight());
blocks.add(bsqBlock);
parseBlock(bsqBlock,
genesisBlockHeight,
genesisTxId,
bsqUTXOMap);
log.info(bsqUTXOMap.toString());
}
printUtxoMap(bsqUTXOMap);
log.info("Took {} ms", System.currentTimeMillis() - startTs);
return bsqUTXOMap;
return new Tuple2<>(blocks, bsqUTXOMap);
} catch (Throwable t) {
throw new BsqBlockchainException(t.getMessage(), t);
}
@ -110,9 +113,6 @@ abstract public class BsqBlockchainService {
txByTxIdMap.entrySet().stream()
.filter(entry -> entry.getKey().equals(genesisTxId))
.forEach(entry -> parseGenesisTx(entry.getValue(), blockHeight, bsqUTXOMap));
// We need to remove the genesis tx from further parsing (would be treated as intraBlockInputTx)
txByTxIdMap.remove(genesisTxId);
}
// Worst case is that all txs in a block are depending on another, so only once get resolved at each iteration.
@ -134,58 +134,50 @@ abstract public class BsqBlockchainService {
// The set of txIds of txs which are used for inputs of another tx in same block
Set<String> intraBlockSpendingTxIdSet = getIntraBlockSpendingTxIdSet(transactions);
Set<Tx> nonIntraBlockInputTxs = new HashSet<>();
Set<Tx> intraBlockInputTxs = new HashSet<>();
Set<Tx> txsWithoutInputsFromSameBlock = new HashSet<>();
Set<Tx> txsWithInputsFromSameBlock = new HashSet<>();
// First we find the txs which have no intra-block inputs
outerLoop:
for (Tx tx : transactions) {
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 intraBlockInputTxs and move to the next
// We add the tx for later parsing to the txsWithInputsFromSameBlock and move to the next
// outerLoop iteration .
intraBlockInputTxs.add(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
// nonIntraBlockInputTxs for first pass of BSQ validation
nonIntraBlockInputTxs.add(tx);
// txsWithoutInputsFromSameBlock for first pass of BSQ validation
txsWithoutInputsFromSameBlock.add(tx);
}
log.debug("intraBlockInputTxs " + intraBlockInputTxs.size());
log.debug("nonIntraBlockInputTxs " + nonIntraBlockInputTxs.size());
// we check if we have any valid BSQ utxo from that tx set
boolean utxoChanged = false;
if (!nonIntraBlockInputTxs.isEmpty()) {
for (Tx tx : nonIntraBlockInputTxs) {
utxoChanged = utxoChanged || updateBsqUtxoMapFromTx(tx, blockHeight, bsqUTXOMap);
if (!txsWithoutInputsFromSameBlock.isEmpty()) {
for (Tx tx : txsWithoutInputsFromSameBlock) {
updateBsqUtxoMapFromTx(tx, blockHeight, bsqUTXOMap);
}
}
// we check if we have any valid BSQ utxo from that tx set
if (!intraBlockInputTxs.isEmpty()) {
if (utxoChanged) {
if (recursionCounter < maxRecursions) {
updateBsqUtxoMapFromBlock(intraBlockInputTxs, bsqUTXOMap, blockHeight, ++recursionCounter, maxRecursions);
} else {
final String msg = "We exceeded our max. recursions for resolveConnectedTxs.\n" +
"intraBlockInputTxs=" + intraBlockInputTxs.toString() + "\n" +
"nonIntraBlockInputTxs=" + nonIntraBlockInputTxs;
log.warn(msg);
if (DevEnv.DEV_MODE)
throw new RuntimeException(msg);
}
// 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, blockHeight, ++recursionCounter, maxRecursions);
} else {
final String msg = "If we have intraBlockInputTxs we must have had got the utxoChanged, otherwise we cannot " +
"satisfy the open intraBlockInputTxs.\n" +
"intraBlockInputTxs=" + intraBlockInputTxs.toString() + "\n" +
"nonIntraBlockInputTxs=" + nonIntraBlockInputTxs;
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.info("We have no more txsWithInputsFromSameBlock.");
}
}
@ -205,14 +197,14 @@ abstract public class BsqBlockchainService {
String txId = tx.getId();
List<TxOutput> outputs = tx.getOutputs();
boolean utxoChanged = false;
Coin availableValue = Coin.ZERO;
long availableValue = 0;
for (TxInput input : tx.getInputs()) {
String spendingTxId = input.getSpendingTxId();
final int spendingTxOutputIndex = input.getSpendingTxOutputIndex();
if (bsqUTXOMap.containsTuple(spendingTxId, spendingTxOutputIndex)) {
BsqUTXO bsqUTXO = bsqUTXOMap.getByTuple(spendingTxId, spendingTxOutputIndex);
log.debug("input value " + bsqUTXO.getValue());
availableValue = availableValue.add(bsqUTXO.getValue());
availableValue = availableValue + bsqUTXO.getValue();
bsqUTXOMap.removeByTuple(spendingTxId, spendingTxOutputIndex);
utxoChanged = true;
@ -223,44 +215,38 @@ abstract public class BsqBlockchainService {
}
// If we have an input spending tokens we iterate the outputs
if (availableValue.isPositive()) {
if (availableValue > 0) {
// We use order of output index. An output is a BSQ utxo as long there is enough input value
for (int outputIndex = 0; outputIndex < outputs.size(); outputIndex++) {
TxOutput txOutput = outputs.get(outputIndex);
availableValue = availableValue - txOutput.getValue();
if (availableValue >= 0) {
// We are spending available tokens
BsqUTXO bsqUTXO = new BsqUTXO(txId,
blockHeight,
false,
txOutput);
bsqUTXOMap.putByTuple(txId, outputIndex, bsqUTXO);
if (availableValue.isZero()) {
log.debug("We don't have anymore BSQ to spend");
break;
} else {
availableValue = availableValue.subtract(txOutput.getValue());
if (!availableValue.isNegative()) {
// We are spending available tokens
BsqUTXO bsqUTXO = new BsqUTXO(txId,
blockHeight,
false,
txOutput);
bsqUTXOMap.putByTuple(txId, outputIndex, bsqUTXO);
if (availableValue.isZero()) {
log.debug("We don't have anymore BSQ to spend");
break;
}
} else {
log.warn("We tried to spend more BSQ as we have in our inputs");
// TODO report burnt BSQ
// TODO: check if we should be more tolerant and use
// availableValue = availableValue.subtract(txOutput.getValue());
// only temp and allow follow up outputs to use the left input.
if (availableValue == 0) {
log.debug("We don't have anymore BSQ to spend");
break;
}
} else {
log.warn("We tried to spend more BSQ as we have in our inputs");
// TODO report burnt BSQ
// TODO: check if we should be more tolerant and use
// availableValue = availableValue.subtract(txOutput.getValue());
// only temp and allow follow up outputs to use the left input.
break;
}
}
//TODO write that warning to a handler
if (availableValue.isPositive()) {
if (availableValue > 0) {
log.warn("BSQ have been left which was not spent. Burned BSQ amount={}, tx={}",
availableValue.value,
availableValue,
tx.toString());
}
}
@ -285,16 +271,4 @@ abstract public class BsqBlockchainService {
}
checkArgument(!bsqUTXOMap.isEmpty(), "Genesis tx need to have BSQ utxo when parsing genesis block");
}
void printUtxoMap(BsqUTXOMap bsqUTXOMap) {
if (log.isInfoEnabled() || log.isDebugEnabled() || log.isTraceEnabled()) {
StringBuilder sb = new StringBuilder("bsqUTXOMap:\n");
bsqUTXOMap.entrySet().stream().forEach(e -> {
sb.append("TxId:Index = ").append(e.getKey()).append("\n");
sb.append(" UTXO = ").append(e.getValue().toString()).append("\n");
}
);
log.error(sb.toString());
}
}
}

View file

@ -18,7 +18,6 @@
package io.bisq.core.dao.blockchain;
import lombok.Value;
import org.bitcoinj.core.Coin;
import org.bitcoinj.script.Script;
import java.util.List;
@ -30,7 +29,7 @@ import java.util.List;
public class BsqUTXO {
private final String txId;
private final long index;
private final Coin value;
private final long value;
private final int height;
private final boolean isBsqCoinBase;
private final Script script;
@ -40,7 +39,7 @@ public class BsqUTXO {
// We do not support raw MS for BSQ but lets see if is needed anyway, might be removed
private final List<String> addresses;
private BsqUTXO(String txId, int index, Coin value, int height, boolean isBsqCoinBase, Script script, List<String> addresses) {
private BsqUTXO(String txId, int index, long value, int height, boolean isBsqCoinBase, Script script, List<String> addresses) {
this.txId = txId;
this.index = index;
this.value = value;
@ -61,4 +60,18 @@ public class BsqUTXO {
output.getScript(),
output.getAddresses());
}
@Override
public String toString() {
return "BsqUTXO{" +
"\n txId='" + txId + '\'' +
",\n index=" + index +
",\n value=" + value +
",\n height=" + height +
",\n isBsqCoinBase=" + isBsqCoinBase +
",\n script=" + script +
",\n utxoId='" + utxoId + '\'' +
",\n addresses=" + addresses +
"\n}";
}
}

View file

@ -38,6 +38,12 @@ public class BsqUTXOMap extends HashMap<BsqUTXOMap.TxIdIndexTuple, BsqUTXO> {
return super.remove(new TxIdIndexTuple(txId, index));
}
@Override
public String toString() {
return "BsqUTXOMap " + super.toString();
}
@Value
static class TxIdIndexTuple {
private final String txId;
@ -45,7 +51,7 @@ public class BsqUTXOMap extends HashMap<BsqUTXOMap.TxIdIndexTuple, BsqUTXO> {
@Override
public String toString() {
return txId + ":" + index;
return "\nTxIdIndexTuple(" + txId + ":" + index + ")";
}
}
}

View file

@ -26,4 +26,15 @@ public class Tx {
private final String id;
private final List<TxInput> inputs;
private final List<TxOutput> outputs;
private final long time; // in ms from epoche
@Override
public String toString() {
return "Tx{" +
"\nid='" + id + '\'' +
",\ninputs=" + inputs +
",\noutputs=" + outputs +
",\ntime=" + time +
"}\n";
}
}

View file

@ -24,4 +24,13 @@ public class TxInput {
private final int spendingTxOutputIndex;
private final String spendingTxId;
private final String txId;
@Override
public String toString() {
return "TxInput{" +
"\nspendingTxOutputIndex=" + spendingTxOutputIndex +
",\nspendingTxId='" + spendingTxId + '\'' +
",\ntxId='" + txId + '\'' +
"}\n";
}
}

View file

@ -18,7 +18,6 @@
package io.bisq.core.dao.blockchain;
import lombok.Value;
import org.bitcoinj.core.Coin;
import org.bitcoinj.script.Script;
import java.util.List;
@ -26,7 +25,17 @@ import java.util.List;
@Value
public class TxOutput {
private final int index;
private final Coin value;
private final long value;
private final List<String> addresses;
private final Script script;
@Override
public String toString() {
return "TxOutput{" +
"\nindex=" + index +
",\nvalue=" + value +
",\naddresses=" + addresses +
",\nscript=" + script +
"}\n";
}
}

View file

@ -19,15 +19,11 @@ package io.bisq.core.user;
import io.bisq.common.app.Version;
import io.bisq.common.persistance.Persistable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class BlockChainExplorer implements Persistable {
// That object is saved to disc. We need to take care of changes to not break deserialization.
private static final long serialVersionUID = Version.LOCAL_DB_VERSION;
private static final Logger log = LoggerFactory.getLogger(BlockChainExplorer.class);
public final String name;
public final String txUrl;
public final String addressUrl;

View file

@ -128,6 +128,8 @@ public final class Preferences implements Persistable {
private final ArrayList<CryptoCurrency> cryptoCurrencies;
private BlockChainExplorer blockChainExplorerMainNet;
private BlockChainExplorer blockChainExplorerTestNet;
private BlockChainExplorer bsqBlockChainExplorer = new BlockChainExplorer("bisq", "https://explorer.bisq.io/tx.html?tx=",
"https://explorer.bisq.io/Address.html?addr=");
private String backupDirectory;
private boolean autoSelectArbitrators = true;
private final Map<String, Boolean> dontShowAgainMap;
@ -135,7 +137,6 @@ public final class Preferences implements Persistable {
private boolean useTorForBitcoinJ = true;
private boolean showOwnOffersInOfferBook = true;
private Locale preferredLocale;
private TradeCurrency preferredTradeCurrency;
private long withdrawalTxFeeInBytes = 100;
private boolean useCustomWithdrawalTxFee = false;
@ -231,16 +232,11 @@ public final class Preferences implements Persistable {
setBlockChainExplorerTestNet(persisted.getBlockChainExplorerTestNet());
setBlockChainExplorerMainNet(persisted.getBlockChainExplorerMainNet());
setBsqBlockChainExplorer(persisted.getBsqBlockChainExplorer());
setUseCustomWithdrawalTxFee(persisted.useCustomWithdrawalTxFee);
setWithdrawalTxFeeInBytes(persisted.withdrawalTxFeeInBytes);
// In case of an older version without that data we set it to defaults
if (blockChainExplorerTestNet == null)
setBlockChainExplorerTestNet(blockChainExplorersTestNet.get(0));
if (blockChainExplorerMainNet == null)
setBlockChainExplorerTestNet(blockChainExplorersMainNet.get(0));
backupDirectory = persisted.getBackupDirectory();
autoSelectArbitrators = persisted.getAutoSelectArbitrators();
dontShowAgainMap = persisted.getDontShowAgainMap();
@ -297,7 +293,6 @@ public final class Preferences implements Persistable {
setBlockChainExplorerMainNet(blockChainExplorersMainNet.get(0));
dontShowAgainMap = new HashMap<>();
preferredLocale = getDefaultLocale();
preferredTradeCurrency = getDefaultTradeCurrency();
maxPriceDistanceInPercent = 0.1;
@ -518,7 +513,12 @@ public final class Preferences implements Persistable {
this.selectedPaymentAccountForCreateOffer = paymentAccount;
storage.queueUpForSave();
}
public void setBsqBlockChainExplorer(BlockChainExplorer bsqBlockChainExplorer) {
this.bsqBlockChainExplorer = bsqBlockChainExplorer;
storage.queueUpForSave();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@ -708,7 +708,11 @@ public final class Preferences implements Persistable {
public PaymentAccount getSelectedPaymentAccountForCreateOffer() {
return selectedPaymentAccountForCreateOffer;
}
public BlockChainExplorer getBsqBlockChainExplorer() {
return bsqBlockChainExplorer;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private

View file

@ -20,6 +20,7 @@ package io.bisq.core.dao.blockchain;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.domain.*;
import io.bisq.common.util.Tuple2;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.junit.After;
@ -87,14 +88,14 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_0);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
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().value);
assertEquals(ADDRESS_GEN_2_VALUE, bsqUTXO2.getValue().value);
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_GEN_2_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
}
@ -126,14 +127,14 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_1);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
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().value);
assertEquals(ADDRESS_TX_1_VALUE, bsqUTXO2.getValue().value);
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_TX_1_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
}
@ -178,14 +179,14 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_1);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
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().value);
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue().value);
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
}
@ -230,14 +231,14 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_2);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
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().value);
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue().value);
assertEquals(ADDRESS_GEN_1_VALUE, bsqUTXO1.getValue());
assertEquals(ADDRESS_TX_2_VALUE, bsqUTXO2.getValue());
assertEquals(2, bsqUTXOMap.size());
}
@ -284,11 +285,11 @@ public class BsqBlockchainServiceTest {
service.buildBlocks(BLOCK_0, BLOCK_1);
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis();
BsqUTXOMap bsqUTXOMap = parseAllBlocksFromGenesis().second;
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().value);
assertEquals(ADDRESS_GEN_1_VALUE + ADDRESS_GEN_2_VALUE, bsqUTXO1.getValue());
assertEquals(1, bsqUTXOMap.size());
}
@ -301,6 +302,7 @@ public class BsqBlockchainServiceTest {
RawTransaction genesisRawTransaction = new RawTransaction();
genesisRawTransaction.setBlockHash("BlockHash" + height);
genesisRawTransaction.setTxId(txId);
genesisRawTransaction.setTime(new Date().getTime() / 1000);
return genesisRawTransaction;
}
@ -376,7 +378,7 @@ public class BsqBlockchainServiceTest {
}
private BsqUTXOMap parseAllBlocksFromGenesis()
private Tuple2<Set<BsqBlock>, BsqUTXOMap> parseAllBlocksFromGenesis()
throws BitcoindException, CommunicationException, BsqBlockchainException {
return service.parseAllBlocksFromGenesis(service.requestChainHeadHeight(),
BLOCK_0,

View file

@ -18,7 +18,6 @@
package io.bisq.gui.main.dao.wallet;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.gui.main.overlays.popups.Popup;
import io.bisq.gui.util.BsqFormatter;
import javafx.scene.control.TextField;
import org.bitcoinj.core.*;
@ -49,6 +48,7 @@ public class BalanceUtil {
public void setBalanceTextField(TextField balanceTextField) {
this.balanceTextField = balanceTextField;
balanceTextField.setMouseTransparent(false);
}
public void initialize() {
@ -91,8 +91,6 @@ public class BalanceUtil {
private void requestUtxo() {
bsqWalletService.requestBsqUtxo(() -> {
balanceTextField.setText(formatter.formatCoinWithCode(bsqWalletService.getAvailableBalance()));
}, errorMessage -> {
new Popup<>().warning(errorMessage);
});
}

View file

@ -27,7 +27,7 @@ import io.bisq.gui.main.dao.DaoView;
import io.bisq.gui.main.dao.wallet.dashboard.BsqDashboardView;
import io.bisq.gui.main.dao.wallet.receive.BsqReceiveView;
import io.bisq.gui.main.dao.wallet.send.BsqSendView;
import io.bisq.gui.main.dao.wallet.tx.BsqTransactionsView;
import io.bisq.gui.main.dao.wallet.tx.BsqTxView;
import io.bisq.gui.util.Colors;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
@ -78,7 +78,7 @@ public class BsqWalletView extends ActivatableViewAndModel {
dashboard = new MenuItem(navigation, toggleGroup, Res.get("shared.dashboard"), BsqDashboardView.class, AwesomeIcon.DASHBOARD);
send = new MenuItem(navigation, toggleGroup, Res.get("dao.wallet.menuItem.send"), BsqSendView.class, AwesomeIcon.SIGNOUT);
receive = new MenuItem(navigation, toggleGroup, Res.get("dao.wallet.menuItem.receive"), BsqReceiveView.class, AwesomeIcon.SIGNIN);
transactions = new MenuItem(navigation, toggleGroup, Res.get("dao.wallet.menuItem.transactions"), BsqTransactionsView.class, AwesomeIcon.TABLE);
transactions = new MenuItem(navigation, toggleGroup, Res.get("dao.wallet.menuItem.transactions"), BsqTxView.class, AwesomeIcon.TABLE);
leftVBox.getChildren().addAll(dashboard, send, receive, transactions);
}
@ -121,7 +121,7 @@ public class BsqWalletView extends ActivatableViewAndModel {
if (view instanceof BsqDashboardView) dashboard.setSelected(true);
else if (view instanceof BsqSendView) send.setSelected(true);
else if (view instanceof BsqReceiveView) receive.setSelected(true);
else if (view instanceof BsqTransactionsView) transactions.setSelected(true);
else if (view instanceof BsqTxView) transactions.setSelected(true);
}
public Class<? extends View> getSelectedViewClass() {

View file

@ -20,12 +20,10 @@ package io.bisq.gui.main.dao.wallet.send;
import com.google.common.util.concurrent.FutureCallback;
import io.bisq.common.app.DevEnv;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.InsufficientFundsException;
import io.bisq.core.btc.exceptions.TransactionVerificationException;
import io.bisq.core.btc.exceptions.WalletException;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.provider.fee.FeeService;
import io.bisq.core.util.CoinUtil;
import io.bisq.gui.common.view.ActivatableView;
import io.bisq.gui.common.view.FxmlView;
@ -35,12 +33,13 @@ import io.bisq.gui.main.overlays.popups.Popup;
import io.bisq.gui.util.BSFormatter;
import io.bisq.gui.util.BsqFormatter;
import io.bisq.gui.util.Layout;
import io.bisq.gui.util.validation.BsqAddressValidator;
import io.bisq.gui.util.validation.BsqValidator;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Transaction;
import org.jetbrains.annotations.NotNull;
@ -52,56 +51,65 @@ import static io.bisq.gui.util.FormBuilder.*;
@FxmlView
public class BsqSendView extends ActivatableView<GridPane, Void> {
private TextField balanceTextField;
private final BsqWalletService bsqWalletService;
private final BtcWalletService btcWalletService;
private final FeeService feeService;
private final BSFormatter bsqFormatter;
private final BSFormatter btcFormatter;
private final BalanceUtil balanceUtil;
private BsqValidator bsqValidator;
private BsqAddressValidator bsqAddressValidator;
private int gridRow = 0;
private InputTextField amountInputTextField;
private Button sendButton;
private InputTextField receiversAddressInputTextField;
private ChangeListener<Boolean> focusOutListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private BsqSendView(BsqWalletService bsqWalletService, BtcWalletService btcWalletService, FeeService feeService, BsqFormatter bsqFormatter, BSFormatter btcFormatter, BalanceUtil balanceUtil) {
private BsqSendView(BsqWalletService bsqWalletService, BtcWalletService btcWalletService,
BsqFormatter bsqFormatter, BSFormatter btcFormatter,
BalanceUtil balanceUtil, BsqValidator bsqValidator, BsqAddressValidator bsqAddressValidator) {
this.bsqWalletService = bsqWalletService;
this.btcWalletService = btcWalletService;
this.feeService = feeService;
this.bsqFormatter = bsqFormatter;
this.btcFormatter = btcFormatter;
this.balanceUtil = balanceUtil;
this.bsqValidator = bsqValidator;
this.bsqAddressValidator = bsqAddressValidator;
}
@Override
public void initialize() {
addTitledGroupBg(root, gridRow, 1, Res.get("shared.balance"));
balanceTextField = addLabelTextField(root, gridRow, Res.get("shared.bsqBalance"), Layout.FIRST_ROW_DISTANCE).second;
TextField balanceTextField = addLabelTextField(root, gridRow, Res.get("shared.bsqBalance"), Layout.FIRST_ROW_DISTANCE).second;
balanceUtil.setBalanceTextField(balanceTextField);
balanceUtil.initialize();
addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.wallet.send.sendFunds"), Layout.GROUP_DISTANCE);
amountInputTextField = addLabelInputTextField(root, gridRow, Res.get("dao.wallet.send.amount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
amountInputTextField.setPromptText(Res.get("dao.wallet.send.setAmount", Transaction.MIN_NONDUST_OUTPUT.value));
amountInputTextField.setValidator(bsqValidator);
receiversAddressInputTextField = addLabelInputTextField(root, ++gridRow,
Res.get("dao.wallet.send.receiverAddress")).second;
receiversAddressInputTextField.setPromptText(Res.get("dao.wallet.send.setDestinationAddress"));
receiversAddressInputTextField.setValidator(bsqAddressValidator);
focusOutListener = (observable, oldValue, newValue) -> {
if (!newValue)
verifyInputs();
};
sendButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send"));
if (DevEnv.DEV_MODE) {
amountInputTextField.setText("2.730"); // 2730 is dust limit
receiversAddressInputTextField.setText("mpaZiEh8gSr4LcH11FrLdRY57aArt88qtg");
receiversAddressInputTextField.setText("mqLgu2GH1pZntcsuugNtZuczLfZx8P5sdK");
}
sendButton.setOnAction((event) -> {
@ -150,11 +158,10 @@ public class BsqSendView extends ActivatableView<GridPane, Void> {
})
.closeButtonText(Res.get("shared.cancel"))
.show();
} catch (AddressFormatException | InsufficientFundsException |
TransactionVerificationException | WalletException | InsufficientMoneyException e) {
log.error(e.toString());
e.printStackTrace();
new Popup<>().warning(e.toString()).show();
} catch (Throwable t) {
log.error(t.toString());
t.printStackTrace();
new Popup<>().warning(t.getMessage()).show();
}
});
}
@ -162,11 +169,22 @@ public class BsqSendView extends ActivatableView<GridPane, Void> {
@Override
protected void activate() {
balanceUtil.activate();
receiversAddressInputTextField.focusedProperty().addListener(focusOutListener);
amountInputTextField.focusedProperty().addListener(focusOutListener);
verifyInputs();
}
@Override
protected void deactivate() {
balanceUtil.deactivate();
receiversAddressInputTextField.focusedProperty().removeListener(focusOutListener);
amountInputTextField.focusedProperty().removeListener(focusOutListener);
}
private void verifyInputs() {
boolean isValid = bsqAddressValidator.validate(receiversAddressInputTextField.getText()).isValid &&
bsqValidator.validate(amountInputTextField.getText()).isValid;
sendButton.setDisable(!isValid);
}
}

View file

@ -1,52 +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.gui.main.dao.wallet.tx;
import io.bisq.gui.common.view.ActivatableView;
import io.bisq.gui.common.view.FxmlView;
import javafx.scene.layout.GridPane;
import javax.inject.Inject;
@FxmlView
public class BsqTransactionsView extends ActivatableView<GridPane, Void> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private BsqTransactionsView() {
}
@Override
public void initialize() {
}
@Override
protected void activate() {
}
@Override
protected void deactivate() {
}
}

View file

@ -0,0 +1,133 @@
/*
* 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.gui.main.dao.wallet.tx;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.listeners.TxConfidenceListener;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.gui.components.indicator.TxConfidenceIndicator;
import io.bisq.gui.util.GUIUtil;
import javafx.scene.control.Tooltip;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionOutput;
import java.util.Date;
import static com.google.common.base.Preconditions.checkNotNull;
@ToString
@Slf4j
class BsqTxListItem {
@Getter
private final Transaction transaction;
private BsqWalletService bsqWalletService;
@Getter
private final Date date;
@Getter
private final String txId;
@Getter
private int confirmations = 0;
@Getter
private final String address;
@Getter
private final String direction;
@Getter
private final Coin amount;
@Getter
private boolean received;
@Getter
private TxConfidenceIndicator txConfidenceIndicator;
private TxConfidenceListener txConfidenceListener;
public BsqTxListItem(Transaction transaction, BsqWalletService bsqWalletService) {
this.transaction = transaction;
this.bsqWalletService = bsqWalletService;
txId = transaction.getHashAsString();
date = transaction.getUpdateTime();
setupConfidence(bsqWalletService);
checkNotNull(transaction, "transaction must not be null as we only have list items from transactions " +
"which are available in the wallet");
Coin valueSentToMe = bsqWalletService.getValueSentToMeForTransaction(transaction);
Coin valueSentFromMe = bsqWalletService.getValueSentFromMeForTransaction(transaction);
if (valueSentToMe.compareTo(valueSentFromMe) > 0) {
amount = valueSentToMe.subtract(valueSentFromMe);
direction = Res.get("funds.tx.direction.receivedWith");
received = true;
} else if (valueSentToMe.compareTo(valueSentFromMe) < 0) {
amount = valueSentFromMe.subtract(valueSentToMe);
direction = Res.get("funds.tx.direction.sentTo");
received = false;
} else {
amount = Coin.ZERO;
direction = "";
}
String result = null;
for (TransactionOutput output : transaction.getOutputs()) {
if (!bsqWalletService.isTransactionOutputMine(output)) {
if (output.getScriptPubKey().isSentToAddress()
|| output.getScriptPubKey().isPayToScriptHash()) {
result = output.getScriptPubKey().getToAddress(bsqWalletService.getParams()).toString();
}
}
}
address = result != null ? result : "";
}
private void setupConfidence(BsqWalletService bsqWalletService) {
txConfidenceIndicator = new TxConfidenceIndicator();
txConfidenceIndicator.setId("funds-confidence");
Tooltip tooltip = new Tooltip();
txConfidenceIndicator.setProgress(0);
txConfidenceIndicator.setPrefHeight(30);
txConfidenceIndicator.setPrefWidth(30);
Tooltip.install(txConfidenceIndicator, tooltip);
txConfidenceListener = new TxConfidenceListener(txId) {
@Override
public void onTransactionConfidenceChanged(TransactionConfidence confidence) {
updateConfidence(confidence, tooltip);
}
};
bsqWalletService.addTxConfidenceListener(txConfidenceListener);
updateConfidence(bsqWalletService.getConfidenceForTxId(txId), tooltip);
}
private void updateConfidence(TransactionConfidence confidence, Tooltip tooltip) {
if (confidence != null) {
GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator);
confirmations = confidence.getDepthInBlocks();
}
}
public void cleanup() {
bsqWalletService.removeTxConfidenceListener(txConfidenceListener);
}
}

View file

@ -18,7 +18,7 @@
-->
<?import javafx.scene.layout.*?>
<GridPane fx:id="root" fx:controller="io.bisq.gui.main.dao.wallet.tx.BsqTransactionsView"
<GridPane fx:id="root" fx:controller="io.bisq.gui.main.dao.wallet.tx.BsqTxView"
hgap="5.0" vgap="5.0"
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="-10.0"

View file

@ -0,0 +1,287 @@
/*
* 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.gui.main.dao.wallet.tx;
import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.user.Preferences;
import io.bisq.gui.common.view.ActivatableView;
import io.bisq.gui.common.view.FxmlView;
import io.bisq.gui.components.AddressWithIconAndDirection;
import io.bisq.gui.components.HyperlinkWithIcon;
import io.bisq.gui.main.dao.wallet.BalanceUtil;
import io.bisq.gui.util.BsqFormatter;
import io.bisq.gui.util.GUIUtil;
import io.bisq.gui.util.Layout;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.util.Callback;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import java.util.Set;
import java.util.stream.Collectors;
import static io.bisq.gui.util.FormBuilder.addLabelTextField;
import static io.bisq.gui.util.FormBuilder.addTitledGroupBg;
@FxmlView
public class BsqTxView extends ActivatableView<GridPane, Void> {
TableView<BsqTxListItem> tableView;
private int gridRow = 0;
private BsqFormatter bsqFormatter;
private BsqWalletService bsqWalletService;
private BalanceUtil balanceUtil;
private Preferences preferences;
private ListChangeListener<Transaction> walletBsqTransactionsListener;
private final ObservableList<BsqTxListItem> observableList = FXCollections.observableArrayList();
private final SortedList<BsqTxListItem> sortedList = new SortedList<>(observableList);
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private BsqTxView(BsqFormatter bsqFormatter, BsqWalletService bsqWalletService,
BalanceUtil balanceUtil, Preferences preferences) {
this.bsqFormatter = bsqFormatter;
this.bsqWalletService = bsqWalletService;
this.balanceUtil = balanceUtil;
this.preferences = preferences;
}
@Override
public void initialize() {
addTitledGroupBg(root, gridRow, 1, Res.get("shared.balance"));
TextField balanceTextField = addLabelTextField(root, gridRow, Res.get("shared.bsqBalance"),
Layout.FIRST_ROW_DISTANCE).second;
balanceUtil.setBalanceTextField(balanceTextField);
balanceUtil.initialize();
tableView = new TableView<>();
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
GridPane.setRowIndex(tableView, ++gridRow);
GridPane.setColumnSpan(tableView, 2);
GridPane.setMargin(tableView, new Insets(40, -10, 5, -10));
root.getChildren().add(tableView);
addDateColumn();
addTxIdColumn();
addAddressColumn();
addAmountColumn();
addConfidenceColumn();
walletBsqTransactionsListener = change -> updateList();
}
@Override
protected void activate() {
balanceUtil.activate();
bsqWalletService.getWalletBsqTransactions().addListener(walletBsqTransactionsListener);
updateList();
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedList);
}
@Override
protected void deactivate() {
balanceUtil.deactivate();
sortedList.comparatorProperty().unbind();
bsqWalletService.getWalletBsqTransactions().removeListener(walletBsqTransactionsListener);
observableList.forEach(BsqTxListItem::cleanup);
}
private void updateList() {
observableList.forEach(BsqTxListItem::cleanup);
Set<BsqTxListItem> list = bsqWalletService.getWalletBsqTransactions().stream()
.map(t -> new BsqTxListItem(t, bsqWalletService))
.collect(Collectors.toSet());
observableList.setAll(list);
}
private void addDateColumn() {
TableColumn<BsqTxListItem, BsqTxListItem> column = new TableColumn<>(Res.get("shared.dateTime"));
column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setMinWidth(180);
column.setMaxWidth(180);
column.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
column.setCellFactory(
new Callback<TableColumn<BsqTxListItem, BsqTxListItem>, TableCell<BsqTxListItem,
BsqTxListItem>>() {
@Override
public TableCell<BsqTxListItem, BsqTxListItem> call(TableColumn<BsqTxListItem,
BsqTxListItem> column) {
return new TableCell<BsqTxListItem, BsqTxListItem>() {
@Override
public void updateItem(final BsqTxListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(bsqFormatter.formatDateTime(item.getDate()));
} else {
setText("");
}
}
};
}
});
tableView.getColumns().add(column);
column.setComparator((o1, o2) -> o1.getDate().compareTo(o2.getDate()));
column.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(column);
}
private void addTxIdColumn() {
TableColumn<BsqTxListItem, BsqTxListItem> column = new TableColumn<>(Res.get("shared.txId"));
column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setMinWidth(60);
column.setCellFactory(
new Callback<TableColumn<BsqTxListItem, BsqTxListItem>, TableCell<BsqTxListItem,
BsqTxListItem>>() {
@Override
public TableCell<BsqTxListItem, BsqTxListItem> call(TableColumn<BsqTxListItem,
BsqTxListItem> column) {
return new TableCell<BsqTxListItem, BsqTxListItem>() {
private HyperlinkWithIcon hyperlinkWithIcon;
@Override
public void updateItem(final BsqTxListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
String transactionId = item.getTxId();
hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK);
hyperlinkWithIcon.setOnAction(event -> openTxInBlockExplorer(item));
hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId)));
setGraphic(hyperlinkWithIcon);
} else {
setGraphic(null);
if (hyperlinkWithIcon != null)
hyperlinkWithIcon.setOnAction(null);
}
}
};
}
});
tableView.getColumns().add(column);
}
private void addAddressColumn() {
TableColumn<BsqTxListItem, BsqTxListItem> column = new TableColumn<>(Res.get("shared.address"));
column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setMinWidth(140);
column.setCellFactory(
new Callback<TableColumn<BsqTxListItem, BsqTxListItem>, TableCell<BsqTxListItem,
BsqTxListItem>>() {
@Override
public TableCell<BsqTxListItem, BsqTxListItem> call(TableColumn<BsqTxListItem,
BsqTxListItem> column) {
return new TableCell<BsqTxListItem, BsqTxListItem>() {
private AddressWithIconAndDirection field;
@Override
public void updateItem(final BsqTxListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
String addressString = item.getAddress();
field = new AddressWithIconAndDirection(item.getDirection(), addressString,
AwesomeIcon.EXTERNAL_LINK, item.isReceived());
field.setOnAction(event -> openAddressInBlockExplorer(item));
field.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForAddress", addressString)));
setGraphic(field);
} else {
setGraphic(null);
if (field != null)
field.setOnAction(null);
}
}
};
}
});
tableView.getColumns().add(column);
}
private void addAmountColumn() {
TableColumn<BsqTxListItem, String> column = new TableColumn<>(Res.get("shared.amountWithCur", "BSQ"));
column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(bsqFormatter.formatCoin(item.getValue().getAmount())));
column.setMinWidth(130);
tableView.getColumns().add(column);
}
private void addConfidenceColumn() {
TableColumn<BsqTxListItem, BsqTxListItem> column = new TableColumn<>(Res.get("shared.confirmations"));
column.setMinWidth(130);
column.setMaxWidth(130);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(new Callback<TableColumn<BsqTxListItem, BsqTxListItem>,
TableCell<BsqTxListItem, BsqTxListItem>>() {
@Override
public TableCell<BsqTxListItem, BsqTxListItem> call(TableColumn<BsqTxListItem,
BsqTxListItem> column) {
return new TableCell<BsqTxListItem, BsqTxListItem>() {
@Override
public void updateItem(final BsqTxListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setGraphic(item.getTxConfidenceIndicator());
} else {
setGraphic(null);
}
}
};
}
});
tableView.getColumns().add(column);
}
private void openTxInBlockExplorer(BsqTxListItem item) {
if (item.getTxId() != null)
GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().txUrl + item.getTxId());
}
private void openAddressInBlockExplorer(BsqTxListItem item) {
if (item.getAddress() != null) {
GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().addressUrl + item.getAddress());
}
}
}

View file

@ -19,6 +19,7 @@ package io.bisq.gui.main.funds.transactions;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.listeners.TxConfidenceListener;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.offer.Offer;
import io.bisq.core.offer.OpenOffer;
@ -28,21 +29,22 @@ import io.bisq.gui.components.indicator.TxConfidenceIndicator;
import io.bisq.gui.util.BSFormatter;
import io.bisq.gui.util.GUIUtil;
import javafx.scene.control.Tooltip;
import org.bitcoinj.core.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionOutput;
import javax.annotation.Nullable;
import java.util.Date;
import java.util.Optional;
@Slf4j
class TransactionsListItem {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private String dateString;
private final Date date;
private final String txId;
private final BtcWalletService walletService;
private final BtcWalletService btcWalletService;
private final TxConfidenceIndicator txConfidenceIndicator;
private final Tooltip tooltip;
@Nullable
@ -53,65 +55,74 @@ class TransactionsListItem {
private TxConfidenceListener txConfidenceListener;
private boolean received;
private boolean detailsAvailable;
private boolean txFeeForBsqPayment = false;
private Coin amountAsCoin = Coin.ZERO;
private BSFormatter formatter;
private int confirmations = 0;
public TransactionsListItem() {
date = null;
walletService = null;
btcWalletService = null;
txConfidenceIndicator = null;
tooltip = null;
txId = null;
}
public TransactionsListItem(Transaction transaction, BtcWalletService walletService, Optional<Tradable> tradableOptional, BSFormatter formatter) {
public TransactionsListItem(Transaction transaction,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
Optional<Tradable> tradableOptional,
BSFormatter formatter) {
this.formatter = formatter;
txId = transaction.getHashAsString();
this.walletService = walletService;
this.btcWalletService = btcWalletService;
Coin valueSentToMe = walletService.getValueSentToMeForTransaction(transaction);
Coin valueSentFromMe = walletService.getValueSentFromMeForTransaction(transaction);
Address address;
Coin valueSentToMe = btcWalletService.getValueSentToMeForTransaction(transaction);
Coin valueSentFromMe = btcWalletService.getValueSentFromMeForTransaction(transaction);
// TODO check and refactor
if (valueSentToMe.isZero()) {
amountAsCoin = valueSentFromMe.multiply(-1);
for (TransactionOutput transactionOutput : transaction.getOutputs()) {
if (!walletService.isTransactionOutputMine(transactionOutput)) {
direction = Res.get("funds.tx.direction.sentTo");
for (TransactionOutput output : transaction.getOutputs()) {
if (!btcWalletService.isTransactionOutputMine(output)) {
received = false;
if (transactionOutput.getScriptPubKey().isSentToAddress()
|| transactionOutput.getScriptPubKey().isPayToScriptHash()) {
address = transactionOutput.getScriptPubKey().getToAddress(walletService.getParams());
addressString = address.toString();
if (bsqWalletService.isTransactionOutputMine(output)) {
txFeeForBsqPayment = true;
} else {
direction = Res.get("funds.tx.direction.sentTo");
if (output.getScriptPubKey().isSentToAddress()
|| output.getScriptPubKey().isPayToScriptHash()) {
addressString = output.getScriptPubKey().getToAddress(btcWalletService.getParams()).toString();
}
}
}
}
} else if (valueSentFromMe.isZero()) {
amountAsCoin = valueSentToMe;
direction = Res.get("funds.tx.direction.receivedWith");
received = true;
for (TransactionOutput transactionOutput : transaction.getOutputs()) {
if (!walletService.isTransactionOutputMine(transactionOutput)) {
if (transactionOutput.getScriptPubKey().isSentToAddress() ||
transactionOutput.getScriptPubKey().isPayToScriptHash()) {
address = transactionOutput.getScriptPubKey().getToAddress(walletService.getParams());
addressString = address.toString();
for (TransactionOutput output : transaction.getOutputs()) {
if (!btcWalletService.isTransactionOutputMine(output)) {
if (output.getScriptPubKey().isSentToAddress() ||
output.getScriptPubKey().isPayToScriptHash()) {
addressString = output.getScriptPubKey().getToAddress(btcWalletService.getParams()).toString();
}
}
}
} else {
amountAsCoin = valueSentToMe.subtract(valueSentFromMe);
boolean outgoing = false;
for (TransactionOutput transactionOutput : transaction.getOutputs()) {
if (!walletService.isTransactionOutputMine(transactionOutput)) {
outgoing = true;
if (transactionOutput.getScriptPubKey().isSentToAddress() ||
transactionOutput.getScriptPubKey().isPayToScriptHash()) {
address = transactionOutput.getScriptPubKey().getToAddress(walletService.getParams());
addressString = address.toString();
for (TransactionOutput output : transaction.getOutputs()) {
if (!btcWalletService.isTransactionOutputMine(output)) {
if (bsqWalletService.isTransactionOutputMine(output)) {
outgoing = false;
txFeeForBsqPayment = true;
} else {
outgoing = true;
if (output.getScriptPubKey().isSentToAddress()
|| output.getScriptPubKey().isPayToScriptHash()) {
addressString = output.getScriptPubKey().getToAddress(btcWalletService.getParams()).toString();
}
}
}
}
@ -121,6 +132,10 @@ class TransactionsListItem {
received = false;
}
}
if (txFeeForBsqPayment) {
direction = Res.get("funds.tx.txFeePaymentForBsqTx");
addressString = "";
}
// confidence
txConfidenceIndicator = new TxConfidenceIndicator();
@ -138,7 +153,7 @@ class TransactionsListItem {
confirmations = confidence.getDepthInBlocks();
}
};
walletService.addTxConfidenceListener(txConfidenceListener);
btcWalletService.addTxConfidenceListener(txConfidenceListener);
TransactionConfidence confidence = transaction.getConfidence();
GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator);
confirmations = confidence.getDepthInBlocks();
@ -182,8 +197,10 @@ class TransactionsListItem {
} else {
if (amountAsCoin.isZero())
details = Res.get("funds.tx.noFundsFromDispute");
else
else if (!txFeeForBsqPayment)
details = received ? Res.get("funds.tx.receivedFunds") : Res.get("funds.tx.withdrawnFromWallet");
else
details = Res.get("funds.tx.txFeePaymentForBsqTx");
}
date = transaction.getUpdateTime();
@ -192,7 +209,7 @@ class TransactionsListItem {
public void cleanup() {
walletService.removeTxConfidenceListener(txConfidenceListener);
btcWalletService.removeTxConfidenceListener(txConfidenceListener);
}

View file

@ -24,6 +24,7 @@ import io.bisq.common.util.Tuple2;
import io.bisq.common.util.Tuple4;
import io.bisq.common.util.Utilities;
import io.bisq.core.arbitration.DisputeManager;
import io.bisq.core.btc.wallet.BsqWalletService;
import io.bisq.core.btc.wallet.BtcWalletService;
import io.bisq.core.offer.OpenOffer;
import io.bisq.core.offer.OpenOfferManager;
@ -80,7 +81,8 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
private final ObservableList<TransactionsListItem> observableList = FXCollections.observableArrayList();
private final SortedList<TransactionsListItem> sortedList = new SortedList<>(observableList);
private final BtcWalletService walletService;
private final BtcWalletService btcWalletService;
private BsqWalletService bsqWalletService;
private final TradeManager tradeManager;
private final OpenOfferManager openOfferManager;
private final ClosedTradableManager closedTradableManager;
@ -100,12 +102,14 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private TransactionsView(BtcWalletService walletService, TradeManager tradeManager, OpenOfferManager openOfferManager,
private TransactionsView(BtcWalletService btcWalletService, BsqWalletService bsqWalletService,
TradeManager tradeManager, OpenOfferManager openOfferManager,
ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager,
BSFormatter formatter, Preferences preferences, TradeDetailsWindow tradeDetailsWindow,
DisputeManager disputeManager, Stage stage,
OfferDetailsWindow offerDetailsWindow) {
this.walletService = walletService;
this.btcWalletService = btcWalletService;
this.bsqWalletService = bsqWalletService;
this.tradeManager = tradeManager;
this.openOfferManager = openOfferManager;
this.closedTradableManager = closedTradableManager;
@ -206,7 +210,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
tableView.setItems(sortedList);
updateList();
walletService.addEventListener(walletEventListener);
btcWalletService.addEventListener(walletEventListener);
scene = root.getScene();
if (scene != null)
@ -241,7 +245,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
protected void deactivate() {
sortedList.comparatorProperty().unbind();
observableList.forEach(TransactionsListItem::cleanup);
walletService.removeEventListener(walletEventListener);
btcWalletService.removeEventListener(walletEventListener);
if (scene != null)
scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
@ -260,7 +264,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
Stream<Tradable> concat3 = Stream.concat(concat2, failedTradesManager.getFailedTrades().stream());
Set<Tradable> all = concat3.collect(Collectors.toSet());
Set<Transaction> transactions = walletService.getTransactions(true);
Set<Transaction> transactions = btcWalletService.getTransactions(true);
List<TransactionsListItem> transactionsListItems = transactions.stream()
.map(transaction -> {
Optional<Tradable> tradableOptional = all.stream()
@ -289,7 +293,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
return false;
})
.findAny();
return new TransactionsListItem(transaction, walletService, tradableOptional, formatter);
return new TransactionsListItem(transaction, btcWalletService, bsqWalletService, tradableOptional, formatter);
})
.collect(Collectors.toList());
@ -524,7 +528,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
TransactionConfidence confidence = walletService.getConfidenceForTxId(item.getTxId());
TransactionConfidence confidence = btcWalletService.getConfidenceForTxId(item.getTxId());
if (confidence != null) {
if (confidence.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING) {
if (button == null) {
@ -555,9 +559,9 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
private void revertTransaction(String txId, @Nullable Tradable tradable) {
try {
walletService.doubleSpendTransaction(txId, () -> {
btcWalletService.doubleSpendTransaction(txId, () -> {
if (tradable != null)
walletService.swapAnyTradeEntryContextToAvailableEntry(tradable.getId());
btcWalletService.swapAnyTradeEntryContextToAvailableEntry(tradable.getId());
new Popup().information(Res.get("funds.tx.txSent")).show();
}, errorMessage -> new Popup().warning(errorMessage).show());

View file

@ -491,7 +491,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
boolean initWithData(Offer.Direction direction, TradeCurrency tradeCurrency) {
boolean result = dataModel.initWithData(direction, tradeCurrency);
if (dataModel.paymentAccount != null)
btcValidator.setMaxValueInBitcoin(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin());
btcValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin());
return result;
}
@ -547,7 +547,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
}
public void onPaymentAccountSelected(PaymentAccount paymentAccount) {
btcValidator.setMaxValueInBitcoin(paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin());
btcValidator.setMaxValue(paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin());
dataModel.onPaymentAccountSelected(paymentAccount);
if (amount.get() != null)
amountValidationResult.set(isBtcInputValid(amount.get()));

View file

@ -175,7 +175,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
offer.errorMessageProperty().addListener(offerErrorListener);
errorMessage.set(offer.getErrorMessage());
btcValidator.setMaxValueInBitcoin(offer.getAmount());
btcValidator.setMaxValue(offer.getAmount());
}
@ -203,7 +203,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
public void onPaymentAccountSelected(PaymentAccount paymentAccount) {
dataModel.onPaymentAccountSelected(paymentAccount);
if (offer != null)
btcValidator.setMaxValueInBitcoin(offer.getAmount());
btcValidator.setMaxValue(offer.getAmount());
}
public void onShowPayFundsScreen() {

View file

@ -29,7 +29,7 @@ public class BsqFormatter extends BSFormatter {
@Inject
private BsqFormatter() {
super();
coinFormat = new MonetaryFormat().shift(5).minDecimals(0).code(5, "BSQ").minDecimals(3);
coinFormat = new MonetaryFormat().shift(5).code(5, "BSQ").minDecimals(3);
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.gui.util.validation;
import io.bisq.common.locale.Res;
import io.bisq.core.user.Preferences;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import javax.inject.Inject;
public final class BsqAddressValidator extends InputValidator {
private final Preferences preferences;
@Inject
public BsqAddressValidator(Preferences preferences) {
this.preferences = preferences;
}
@Override
public ValidationResult validate(String input) {
ValidationResult result = validateIfNotEmpty(input);
if (result.isValid)
return validateBsqAddress(input);
else
return result;
}
private ValidationResult validateBsqAddress(String input) {
try {
new Address(preferences.getBitcoinNetwork().getParameters(), input);
return new ValidationResult(true);
} catch (AddressFormatException e) {
return new ValidationResult(false, Res.get("validation.bsq.invalidFormat"));
}
}
}

View file

@ -0,0 +1,93 @@
/*
* 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.gui.util.validation;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.Restrictions;
import io.bisq.gui.util.BsqFormatter;
import org.bitcoinj.core.Coin;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.math.BigDecimal;
public class BsqValidator extends NumberValidator {
protected final BsqFormatter formatter;
@Nullable
protected Coin maxValue;
@Inject
public BsqValidator(BsqFormatter formatter) {
this.formatter = formatter;
setMaxValue(formatter.parseToCoin("2300000")); // TODO make it lower
}
public void setMaxValue(@NotNull Coin maxValue) {
this.maxValue = maxValue;
}
@Override
public ValidationResult validate(String input) {
ValidationResult result = validateIfNotEmpty(input);
if (result.isValid) {
input = cleanInput(input);
result = validateIfNumber(input);
}
if (result.isValid) {
result = validateIfNotZero(input)
.and(validateIfNotNegative(input))
.and(validateIfNotFractionalBtcValue(input))
.and(validateIfNotExceedsMaxBtcValue(input))
.and(validateIfAboveDust(input));
}
return result;
}
protected ValidationResult validateIfAboveDust(String input) {
final Coin coin = formatter.parseToCoin(input);
if (Restrictions.isAboveDust(coin))
return new ValidationResult(true);
else
return new ValidationResult(false, Res.get("validation.btc.amountBelowDust"));
}
protected ValidationResult validateIfNotFractionalBtcValue(String input) {
BigDecimal bd = new BigDecimal(input);
final BigDecimal satoshis = bd.movePointRight(3);
if (satoshis.scale() > 0)
return new ValidationResult(false, Res.get("validation.btc.toSmall"));
else
return new ValidationResult(true);
}
protected ValidationResult validateIfNotExceedsMaxBtcValue(String input) {
try {
final Coin coin = formatter.parseToCoin(input);
if (maxValue != null && coin.compareTo(maxValue) > 0)
return new ValidationResult(false, Res.get("validation.btc.toLarge", formatter.formatCoinWithCode(maxValue)));
else
return new ValidationResult(true);
} catch (Throwable t) {
return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage()));
}
}
}

View file

@ -18,8 +18,10 @@
package io.bisq.gui.util.validation;
import io.bisq.common.locale.Res;
import io.bisq.core.btc.Restrictions;
import io.bisq.gui.util.BSFormatter;
import org.bitcoinj.core.Coin;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import javax.inject.Inject;
@ -31,15 +33,15 @@ public class BtcValidator extends NumberValidator {
@Nullable
protected Coin maxValueInBitcoin;
protected Coin maxValue;
@Inject
public BtcValidator(BSFormatter formatter) {
this.formatter = formatter;
}
public void setMaxValueInBitcoin(Coin maxValueInBitcoin) {
this.maxValueInBitcoin = maxValueInBitcoin;
public void setMaxValue(@NotNull Coin maxValue) {
this.maxValue = maxValue;
}
@Override
@ -54,12 +56,21 @@ public class BtcValidator extends NumberValidator {
result = validateIfNotZero(input)
.and(validateIfNotNegative(input))
.and(validateIfNotFractionalBtcValue(input))
.and(validateIfNotExceedsMaxBtcValue(input));
.and(validateIfNotExceedsMaxBtcValue(input))
.and(validateIfAboveDust(input));
}
return result;
}
protected ValidationResult validateIfAboveDust(String input) {
final Coin coin = Coin.parseCoin(input);
if (Restrictions.isAboveDust(coin))
return new ValidationResult(true);
else
return new ValidationResult(false, Res.get("validation.btc.amountBelowDust"));
}
protected ValidationResult validateIfNotFractionalBtcValue(String input) {
BigDecimal bd = new BigDecimal(input);
final BigDecimal satoshis = bd.movePointRight(8);
@ -72,8 +83,8 @@ public class BtcValidator extends NumberValidator {
protected ValidationResult validateIfNotExceedsMaxBtcValue(String input) {
try {
final Coin coin = Coin.parseCoin(input);
if (maxValueInBitcoin != null && coin.compareTo(maxValueInBitcoin) > 0)
return new ValidationResult(false, Res.get("validation.btc.toLarge", formatter.formatCoinWithCode(maxValueInBitcoin)));
if (maxValue != null && coin.compareTo(maxValue) > 0)
return new ValidationResult(false, Res.get("validation.btc.toLarge", formatter.formatCoinWithCode(maxValue)));
else
return new ValidationResult(true);
} catch (Throwable t) {

View file

@ -29,7 +29,7 @@ public class SecurityDepositValidator extends BtcValidator {
@Inject
public SecurityDepositValidator(BSFormatter formatter) {
super(formatter);
setMaxValueInBitcoin(Restrictions.MAX_BUYER_SECURITY_DEPOSIT);
setMaxValue(Restrictions.MAX_BUYER_SECURITY_DEPOSIT);
}