Merge branch 'master' into 02-scripted-bot-test

This commit is contained in:
ghubstan 2021-02-25 10:08:32 -03:00
commit b341bb6e89
No known key found for this signature in database
GPG key ID: E35592D6800A861E
23 changed files with 579 additions and 119 deletions

View file

@ -73,6 +73,6 @@ public enum BaseCurrencyNetwork {
}
public long getDefaultMinFeePerVbyte() {
return 2;
return 15; // 2021-02-22 due to mempool congestion, increased from 2
}
}

View file

@ -196,12 +196,12 @@ public class DomainInitialisation {
tradeLimits.onAllServicesInitialized();
tradeManager.onAllServicesInitialized();
arbitrationManager.onAllServicesInitialized();
mediationManager.onAllServicesInitialized();
refundManager.onAllServicesInitialized();
traderChatManager.onAllServicesInitialized();
tradeManager.onAllServicesInitialized();
closedTradableManager.onAllServicesInitialized();
failedTradesManager.onAllServicesInitialized();
xmrTxProofService.onAllServicesInitialized();

View file

@ -33,8 +33,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/**
* Used from org.bitcoinj.wallet.DefaultCoinSelector but added selectOutput method and changed static methods to
* instance methods.
@ -49,6 +52,12 @@ public abstract class BisqDefaultCoinSelector implements CoinSelector {
protected final boolean permitForeignPendingTx;
// TransactionOutputs to be used as candidates in the select method.
// We reset the value to null just after we have applied it inside the select method.
@Nullable
@Setter
protected Set<TransactionOutput> utxoCandidates;
public CoinSelection select(Coin target, Set<TransactionOutput> candidates) {
return select(target, new ArrayList<>(candidates));
}
@ -65,7 +74,16 @@ public abstract class BisqDefaultCoinSelector implements CoinSelector {
public CoinSelection select(Coin target, List<TransactionOutput> candidates) {
ArrayList<TransactionOutput> selected = new ArrayList<>();
// Sort the inputs by age*value so we get the highest "coin days" spent.
ArrayList<TransactionOutput> sortedOutputs = new ArrayList<>(candidates);
ArrayList<TransactionOutput> sortedOutputs;
if (utxoCandidates != null) {
sortedOutputs = new ArrayList<>(utxoCandidates);
// We reuse the selectors. Reset the transactionOutputCandidates field
utxoCandidates = null;
} else {
sortedOutputs = new ArrayList<>(candidates);
}
// If we spend all we don't need to sort
if (!target.equals(NetworkParameters.MAX_MONEY))
sortOutputs(sortedOutputs);
@ -120,6 +138,9 @@ public abstract class BisqDefaultCoinSelector implements CoinSelector {
abstract boolean isTxOutputSpendable(TransactionOutput output);
// TODO Why it uses coin age and not try to minimize number of inputs as the highest priority?
// Asked Oscar and he also don't knows why coin age is used. Should be changed so that min. number of inputs is
// target.
protected void sortOutputs(ArrayList<TransactionOutput> outputs) {
Collections.sort(outputs, (a, b) -> {
int depth1 = a.getParentTransactionDepthInBlocks();

View file

@ -72,6 +72,8 @@ import java.util.stream.Stream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.bitcoinj.core.TransactionConfidence.ConfidenceType.BUILDING;
@ -135,6 +137,8 @@ public class BsqWalletService extends WalletService implements DaoStateListener
this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService;
this.daoKillSwitch = daoKillSwitch;
nonBsqCoinSelector.setPreferences(preferences);
walletsSetup.addSetupCompletedHandler(() -> {
wallet = walletsSetup.getBsqWallet();
if (wallet != null) {
@ -313,6 +317,16 @@ public class BsqWalletService extends WalletService implements DaoStateListener
walletTransactionsChangeListeners.remove(listener);
}
public List<TransactionOutput> getSpendableBsqTransactionOutputs() {
return new ArrayList<>(bsqCoinSelector.select(NetworkParameters.MAX_MONEY,
wallet.calculateAllSpendCandidates()).gathered);
}
public List<TransactionOutput> getSpendableNonBsqTransactionOutputs() {
return new ArrayList<>(nonBsqCoinSelector.select(NetworkParameters.MAX_MONEY,
wallet.calculateAllSpendCandidates()).gathered);
}
///////////////////////////////////////////////////////////////////////////////////////////
// BSQ TransactionOutputs and Transactions
@ -511,7 +525,19 @@ public class BsqWalletService extends WalletService implements DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction getPreparedSendBsqTx(String receiverAddress, Coin receiverAmount)
throws AddressFormatException, InsufficientBsqException, WalletException, TransactionVerificationException, BsqChangeBelowDustException {
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
return getPreparedSendTx(receiverAddress, receiverAmount, bsqCoinSelector, false);
}
public Transaction getPreparedSendBsqTx(String receiverAddress,
Coin receiverAmount,
@Nullable Set<TransactionOutput> utxoCandidates)
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
if (utxoCandidates != null) {
bsqCoinSelector.setUtxoCandidates(utxoCandidates);
}
return getPreparedSendTx(receiverAddress, receiverAmount, bsqCoinSelector, false);
}
@ -520,7 +546,19 @@ public class BsqWalletService extends WalletService implements DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction getPreparedSendBtcTx(String receiverAddress, Coin receiverAmount)
throws AddressFormatException, InsufficientBsqException, WalletException, TransactionVerificationException, BsqChangeBelowDustException {
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
return getPreparedSendTx(receiverAddress, receiverAmount, nonBsqCoinSelector, true);
}
public Transaction getPreparedSendBtcTx(String receiverAddress,
Coin receiverAmount,
@Nullable Set<TransactionOutput> utxoCandidates)
throws AddressFormatException, InsufficientBsqException, WalletException,
TransactionVerificationException, BsqChangeBelowDustException {
if (utxoCandidates != null) {
nonBsqCoinSelector.setUtxoCandidates(utxoCandidates);
}
return getPreparedSendTx(receiverAddress, receiverAmount, nonBsqCoinSelector, true);
}

View file

@ -19,6 +19,7 @@ package bisq.core.btc.wallet;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.TxOutputKey;
import bisq.core.user.Preferences;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
@ -26,6 +27,7 @@ import org.bitcoinj.core.TransactionOutput;
import javax.inject.Inject;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
@ -35,6 +37,8 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class NonBsqCoinSelector extends BisqDefaultCoinSelector {
private DaoStateService daoStateService;
@Setter
private Preferences preferences;
@Inject
public NonBsqCoinSelector(DaoStateService daoStateService) {
@ -60,9 +64,9 @@ public class NonBsqCoinSelector extends BisqDefaultCoinSelector {
return !daoStateService.existsTxOutput(key) || daoStateService.isRejectedIssuanceOutput(key);
}
// BTC utxo in the BSQ wallet are usually from rejected comp request so we don't expect dust attack utxos here.
// Prevent usage of dust attack utxos
@Override
protected boolean isDustAttackUtxo(TransactionOutput output) {
return false;
return output.getValue().value < preferences.getIgnoreDustThreshold();
}
}

View file

@ -139,6 +139,11 @@ public class TraderChatManager extends SupportManager {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
super.onAllServicesInitialized();
tryApplyMessages();
}
public void onSupportMessage(SupportMessage message) {
if (canProcessMessage(message)) {
log.info("Received {} with tradeId {} and uid {}",

View file

@ -78,6 +78,7 @@ shared.offerType=Offer type
shared.details=Details
shared.address=Address
shared.balanceWithCur=Balance in {0}
shared.utxo=Unspent transaction output
shared.txId=Transaction ID
shared.confirmations=Confirmations
shared.revert=Revert Tx
@ -2270,6 +2271,7 @@ dao.wallet.send.receiverAddress=Receiver's BSQ address
dao.wallet.send.receiverBtcAddress=Receiver's BTC address
dao.wallet.send.setDestinationAddress=Fill in your destination address
dao.wallet.send.send=Send BSQ funds
dao.wallet.send.inputControl=Select inputs
dao.wallet.send.sendBtc=Send BTC funds
dao.wallet.send.sendFunds.headline=Confirm withdrawal request
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
@ -2487,6 +2489,9 @@ dao.factsAndFigures.transactions.irregularTx=No. of all irregular transactions
# Windows
####################################################################
inputControlWindow.headline=Select inputs for transaction
inputControlWindow.balanceLabel=Available balance
contractWindow.title=Dispute details
contractWindow.dates=Offer date / Trade date
contractWindow.btcAddresses=Bitcoin address BTC buyer / BTC seller
@ -3619,6 +3624,7 @@ validation.inputTooSmall=Input has to be larger than {0}
validation.inputToBeAtLeast=Input has to be at least {0}
validation.amountBelowDust=An amount below the dust limit of {0} satoshi is not allowed.
validation.length=Length must be between {0} and {1}
validation.fixedLength=Length must be {0}
validation.pattern=Input must be of format: {0}
validation.noHexString=The input is not in HEX format.
validation.advancedCash.invalidFormat=Must be a valid email or wallet id of format: X000000000000

View file

@ -76,7 +76,7 @@ public class InputTextField extends JFXTextField {
validationResult.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
resetValidation();
jfxValidationWrapper.resetValidation();
if (!newValue.isValid) {
if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking
validate(); // ensure that the new error message replaces the old one
@ -92,9 +92,7 @@ public class InputTextField extends JFXTextField {
});
textProperty().addListener((o, oldValue, newValue) -> {
if (validator != null) {
this.validationResult.set(validator.validate(getText()));
}
refreshValidation();
});
focusedProperty().addListener((o, oldValue, newValue) -> {
@ -108,6 +106,7 @@ public class InputTextField extends JFXTextField {
});
}
public InputTextField(double inputLineExtension) {
this();
this.inputLineExtension = inputLineExtension;
@ -119,6 +118,19 @@ public class InputTextField extends JFXTextField {
public void resetValidation() {
jfxValidationWrapper.resetValidation();
String input = getText();
if (input.isEmpty()) {
validationResult.set(new InputValidator.ValidationResult(true));
} else {
validationResult.set(validator.validate(input));
}
}
public void refreshValidation() {
if (validator != null) {
this.validationResult.set(validator.validate(getText()));
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -28,7 +28,9 @@ import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.deposit.DepositView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.TxDetailsBsq;
import bisq.desktop.main.overlays.windows.TxInputSelectionWindow;
import bisq.desktop.main.overlays.windows.WalletPasswordWindow;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.BsqAddressValidator;
@ -47,6 +49,7 @@ import bisq.core.btc.wallet.WalletsManager;
import bisq.core.dao.state.model.blockchain.TxType;
import bisq.core.locale.Res;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.BsqFormatter;
@ -58,10 +61,12 @@ import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.handlers.ResultHandler;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import javax.inject.Inject;
import javax.inject.Named;
@ -71,9 +76,14 @@ import javafx.scene.layout.GridPane;
import javafx.beans.value.ChangeListener;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.addButtonAfterGroup;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
@ -92,15 +102,20 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
private final BtcValidator btcValidator;
private final BsqAddressValidator bsqAddressValidator;
private final BtcAddressValidator btcAddressValidator;
private final Preferences preferences;
private final WalletPasswordWindow walletPasswordWindow;
private int gridRow = 0;
private InputTextField amountInputTextField, btcAmountInputTextField;
private Button sendBsqButton, sendBtcButton;
private Button sendBsqButton, sendBtcButton, bsqInputControlButton, btcInputControlButton;
private InputTextField receiversAddressInputTextField, receiversBtcAddressInputTextField;
private ChangeListener<Boolean> focusOutListener;
private TitledGroupBg btcTitledGroupBg;
private ChangeListener<String> inputTextFieldListener;
@Nullable
private Set<TransactionOutput> bsqUtxoCandidates;
@Nullable
private Set<TransactionOutput> btcUtxoCandidates;
///////////////////////////////////////////////////////////////////////////////////////////
@ -121,6 +136,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
BtcValidator btcValidator,
BsqAddressValidator bsqAddressValidator,
BtcAddressValidator btcAddressValidator,
Preferences preferences,
WalletPasswordWindow walletPasswordWindow) {
this.bsqWalletService = bsqWalletService;
this.btcWalletService = btcWalletService;
@ -135,6 +151,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
this.btcValidator = btcValidator;
this.bsqAddressValidator = bsqAddressValidator;
this.btcAddressValidator = btcAddressValidator;
this.preferences = preferences;
this.walletPasswordWindow = walletPasswordWindow;
}
@ -159,6 +176,16 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
setSendBtcGroupVisibleState(false);
bsqBalanceUtil.activate();
receiversAddressInputTextField.resetValidation();
amountInputTextField.resetValidation();
receiversBtcAddressInputTextField.resetValidation();
btcAmountInputTextField.resetValidation();
sendBsqButton.setOnAction((event) -> onSendBsq());
bsqInputControlButton.setOnAction((event) -> onBsqInputControl());
sendBtcButton.setOnAction((event) -> onSendBtc());
btcInputControlButton.setOnAction((event) -> onBtcInputControl());
receiversAddressInputTextField.focusedProperty().addListener(focusOutListener);
amountInputTextField.focusedProperty().addListener(focusOutListener);
receiversBtcAddressInputTextField.focusedProperty().addListener(focusOutListener);
@ -171,11 +198,16 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
bsqWalletService.addBsqBalanceListener(this);
// We reset the input selection at activate to have all inputs selected, otherwise the user
// might get confused if he had deselected inputs earlier and cannot spend the full balance.
bsqUtxoCandidates = null;
btcUtxoCandidates = null;
onUpdateBalances();
}
private void onUpdateBalances() {
onUpdateBalances(bsqWalletService.getAvailableConfirmedBalance(),
onUpdateBalances(getSpendableBsqBalance(),
bsqWalletService.getAvailableNonBsqBalance(),
bsqWalletService.getUnverifiedBalance(),
bsqWalletService.getUnconfirmedChangeBalance(),
@ -200,6 +232,11 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
btcAmountInputTextField.textProperty().removeListener(inputTextFieldListener);
bsqWalletService.removeBsqBalanceListener(this);
sendBsqButton.setOnAction(null);
btcInputControlButton.setOnAction(null);
sendBtcButton.setOnAction(null);
bsqInputControlButton.setOnAction(null);
}
@Override
@ -210,16 +247,24 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
Coin lockedForVotingBalance,
Coin lockupBondsBalance,
Coin unlockingBondsBalance) {
updateBsqValidator(availableConfirmedBalance);
updateBtcValidator(availableNonBsqBalance);
setSendBtcGroupVisibleState(availableNonBsqBalance.isPositive());
}
private void updateBsqValidator(Coin availableConfirmedBalance) {
bsqValidator.setAvailableBalance(availableConfirmedBalance);
boolean isValid = bsqAddressValidator.validate(receiversAddressInputTextField.getText()).isValid &&
bsqValidator.validate(amountInputTextField.getText()).isValid;
sendBsqButton.setDisable(!isValid);
}
boolean isBtcValid = btcAddressValidator.validate(receiversBtcAddressInputTextField.getText()).isValid &&
private void updateBtcValidator(Coin availableConfirmedBalance) {
btcValidator.setMaxValue(availableConfirmedBalance);
boolean isValid = btcAddressValidator.validate(receiversBtcAddressInputTextField.getText()).isValid &&
btcValidator.validate(btcAmountInputTextField.getText()).isValid;
sendBtcButton.setDisable(!isBtcValid);
setSendBtcGroupVisibleState(availableNonBsqBalance.isPositive());
sendBtcButton.setDisable(!isValid);
}
private void addSendBsqGroup() {
@ -240,9 +285,10 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
onUpdateBalances();
};
sendBsqButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.send"));
sendBsqButton.setOnAction((event) -> onSendBsq());
Tuple2<Button, Button> tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow,
Res.get("dao.wallet.send.send"), Res.get("dao.wallet.send.inputControl"));
sendBsqButton = tuple.first;
bsqInputControlButton = tuple.second;
}
private void onSendBsq() {
@ -253,7 +299,8 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString();
Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount);
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString,
receiverAmount, bsqUtxoCandidates);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
@ -267,12 +314,11 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
bsqFormatter,
btcFormatter,
() -> {
receiversAddressInputTextField.setValidator(null);
receiversAddressInputTextField.setText("");
receiversAddressInputTextField.setValidator(bsqAddressValidator);
amountInputTextField.setValidator(null);
amountInputTextField.setText("");
amountInputTextField.setValidator(bsqValidator);
receiversAddressInputTextField.resetValidation();
amountInputTextField.resetValidation();
});
} catch (BsqChangeBelowDustException e) {
String msg = Res.get("popup.warning.bsqChangeBelowDustException", bsqFormatter.formatCoinWithCode(e.getOutputValue()));
@ -282,16 +328,49 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
}
}
private void onBsqInputControl() {
List<TransactionOutput> unspentTransactionOutputs = bsqWalletService.getSpendableBsqTransactionOutputs();
if (bsqUtxoCandidates == null) {
bsqUtxoCandidates = new HashSet<>(unspentTransactionOutputs);
} else {
// If we had some selection stored we need to update to already spent entries
bsqUtxoCandidates = bsqUtxoCandidates.stream().
filter(e -> unspentTransactionOutputs.contains(e)).
collect(Collectors.toSet());
}
TxInputSelectionWindow txInputSelectionWindow = new TxInputSelectionWindow(unspentTransactionOutputs,
bsqUtxoCandidates,
preferences,
bsqFormatter);
txInputSelectionWindow.onAction(() -> setBsqUtxoCandidates(txInputSelectionWindow.getCandidates()))
.show();
}
private void setBsqUtxoCandidates(Set<TransactionOutput> candidates) {
this.bsqUtxoCandidates = candidates;
updateBsqValidator(getSpendableBsqBalance());
amountInputTextField.refreshValidation();
}
// We have used input selection it is the sum of our selected inputs, otherwise the availableConfirmedBalance
private Coin getSpendableBsqBalance() {
return bsqUtxoCandidates != null ?
Coin.valueOf(bsqUtxoCandidates.stream().mapToLong(e -> e.getValue().value).sum()) :
bsqWalletService.getAvailableConfirmedBalance();
}
private void setSendBtcGroupVisibleState(boolean visible) {
btcTitledGroupBg.setVisible(visible);
receiversBtcAddressInputTextField.setVisible(visible);
btcAmountInputTextField.setVisible(visible);
sendBtcButton.setVisible(visible);
btcInputControlButton.setVisible(visible);
btcTitledGroupBg.setManaged(visible);
receiversBtcAddressInputTextField.setManaged(visible);
btcAmountInputTextField.setManaged(visible);
sendBtcButton.setManaged(visible);
btcInputControlButton.setManaged(visible);
}
private void addSendBtcGroup() {
@ -306,43 +385,81 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
btcAmountInputTextField.setValidator(btcValidator);
GridPane.setColumnSpan(btcAmountInputTextField, 3);
sendBtcButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.sendBtc"));
Tuple2<Button, Button> tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow,
Res.get("dao.wallet.send.sendBtc"), Res.get("dao.wallet.send.inputControl"));
sendBtcButton = tuple.first;
btcInputControlButton = tuple.second;
}
sendBtcButton.setOnAction((event) -> {
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
String receiversAddressString = receiversBtcAddressInputTextField.getText();
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
private void onBtcInputControl() {
List<TransactionOutput> unspentTransactionOutputs = bsqWalletService.getSpendableNonBsqTransactionOutputs();
if (btcUtxoCandidates == null) {
btcUtxoCandidates = new HashSet<>(unspentTransactionOutputs);
} else {
// If we had some selection stored we need to update to already spent entries
btcUtxoCandidates = btcUtxoCandidates.stream().
filter(e -> unspentTransactionOutputs.contains(e)).
collect(Collectors.toSet());
}
TxInputSelectionWindow txInputSelectionWindow = new TxInputSelectionWindow(unspentTransactionOutputs,
btcUtxoCandidates,
preferences,
btcFormatter);
txInputSelectionWindow.onAction(() -> setBtcUtxoCandidates(txInputSelectionWindow.getCandidates())).
show();
}
if (miningFee.getValue() >= receiverAmount.getValue())
GUIUtil.showWantToBurnBTCPopup(miningFee, receiverAmount, btcFormatter);
else {
int txVsize = signedTx.getVsize();
showPublishTxPopup(receiverAmount,
txWithBtcFee,
TxType.INVALID,
miningFee,
txVsize, receiversBtcAddressInputTextField.getText(),
btcFormatter,
btcFormatter,
() -> {
receiversBtcAddressInputTextField.setText("");
btcAmountInputTextField.setText("");
});
private void setBtcUtxoCandidates(Set<TransactionOutput> candidates) {
this.btcUtxoCandidates = candidates;
updateBtcValidator(getSpendableBtcBalance());
btcAmountInputTextField.refreshValidation();
}
}
} catch (BsqChangeBelowDustException e) {
String msg = Res.get("popup.warning.btcChangeBelowDustException", btcFormatter.formatCoinWithCode(e.getOutputValue()));
new Popup().warning(msg).show();
} catch (Throwable t) {
handleError(t);
}
private Coin getSpendableBtcBalance() {
return btcUtxoCandidates != null ?
Coin.valueOf(btcUtxoCandidates.stream().mapToLong(e -> e.getValue().value).sum()) :
bsqWalletService.getAvailableNonBsqBalance();
}
private void onSendBtc() {
if (!GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
return;
}
String receiversAddressString = receiversBtcAddressInputTextField.getText();
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount, btcUtxoCandidates);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
if (miningFee.getValue() >= receiverAmount.getValue())
GUIUtil.showWantToBurnBTCPopup(miningFee, receiverAmount, btcFormatter);
else {
int txVsize = signedTx.getVsize();
showPublishTxPopup(receiverAmount,
txWithBtcFee,
TxType.INVALID,
miningFee,
txVsize, receiversBtcAddressInputTextField.getText(),
btcFormatter,
btcFormatter,
() -> {
receiversBtcAddressInputTextField.setText("");
btcAmountInputTextField.setText("");
receiversBtcAddressInputTextField.resetValidation();
btcAmountInputTextField.resetValidation();
});
}
});
} catch (BsqChangeBelowDustException e) {
String msg = Res.get("popup.warning.btcChangeBelowDustException", btcFormatter.formatCoinWithCode(e.getOutputValue()));
new Popup().warning(msg).show();
} catch (Throwable t) {
handleError(t);
}
}
private void handleError(Throwable t) {
@ -415,4 +532,3 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
walletsManager.publishAndCommitBsqTx(txWithBtcFee, txType, callback);
}
}

View file

@ -0,0 +1,246 @@
/*
* 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 bisq.desktop.main.overlays.windows;
import bisq.desktop.components.AutoTooltipCheckBox;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.BalanceTextField;
import bisq.desktop.components.ExternalHyperlink;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.core.locale.Res;
import bisq.core.user.Preferences;
import bisq.core.util.coin.CoinFormatter;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.TransactionOutput;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.geometry.Insets;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.util.Callback;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
public class TxInputSelectionWindow extends Overlay<TxInputSelectionWindow> {
private static class TransactionOutputItem {
@Getter
private final TransactionOutput transactionOutput;
@Getter
@Setter
private boolean isSelected;
public TransactionOutputItem(TransactionOutput transactionOutput, boolean isSelected) {
this.transactionOutput = transactionOutput;
this.isSelected = isSelected;
}
}
private final List<TransactionOutput> spendableTransactionOutputs;
@Getter
private final Set<TransactionOutput> candidates;
private final Preferences preferences;
private final CoinFormatter formatter;
private BalanceTextField balanceTextField;
private TableView<TransactionOutputItem> tableView;
public TxInputSelectionWindow(List<TransactionOutput> spendableTransactionOutputs,
Set<TransactionOutput> candidates,
Preferences preferences,
CoinFormatter formatter) {
this.spendableTransactionOutputs = spendableTransactionOutputs;
this.candidates = candidates;
this.preferences = preferences;
this.formatter = formatter;
type = Type.Attention;
}
public void show() {
rowIndex = 0;
width = 900;
if (headLine == null) {
headLine = Res.get("inputControlWindow.headline");
}
createGridPane();
gridPane.setHgap(15);
addHeadLine();
addContent();
addButtons();
addDontShowAgainCheckBox();
applyStyles();
display();
}
protected void addContent() {
tableView = new TableView<>();
tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData")));
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
GridPane.setRowIndex(tableView, rowIndex++);
GridPane.setMargin(tableView, new Insets(Layout.GROUP_DISTANCE, 0, 0, 0));
GridPane.setColumnSpan(tableView, 2);
GridPane.setVgrow(tableView, Priority.ALWAYS);
gridPane.getChildren().add(tableView);
createColumns();
ObservableList<TransactionOutputItem> items = FXCollections.observableArrayList(spendableTransactionOutputs.stream()
.map(transactionOutput -> new TransactionOutputItem(transactionOutput, candidates.contains(transactionOutput)))
.collect(Collectors.toList()));
tableView.setItems(new SortedList<>(items));
GUIUtil.setFitToRowsForTableView(tableView, 26, 28, 0, items.size());
balanceTextField = FormBuilder.addBalanceTextField(gridPane, rowIndex++, Res.get("inputControlWindow.balanceLabel"), Layout.FIRST_ROW_DISTANCE);
balanceTextField.setFormatter(formatter);
updateBalance();
}
private void updateBalance() {
balanceTextField.setBalance(Coin.valueOf(candidates.stream()
.mapToLong(transactionOutput -> transactionOutput.getValue().value)
.sum()));
}
private void onChangeCheckBox(TransactionOutputItem transactionOutputItem) {
if (transactionOutputItem.isSelected()) {
candidates.add(transactionOutputItem.getTransactionOutput());
} else {
candidates.remove(transactionOutputItem.getTransactionOutput());
}
updateBalance();
}
private void createColumns() {
TableColumn<TransactionOutputItem, TransactionOutputItem> column;
column = new AutoTooltipTableColumn<>(Res.get("shared.select"));
column.getStyleClass().add("first-column");
column.setSortable(false);
column.setMinWidth(60);
column.setMaxWidth(column.getMinWidth());
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionOutputItem, TransactionOutputItem> call(
TableColumn<TransactionOutputItem, TransactionOutputItem> column) {
return new TableCell<>() {
@Override
public void updateItem(TransactionOutputItem item, boolean empty) {
super.updateItem(item, empty);
final CheckBox checkBox = new AutoTooltipCheckBox();
if (item != null && !empty) {
checkBox.setSelected(item.isSelected());
checkBox.setOnAction(e -> {
item.setSelected(checkBox.isSelected());
onChangeCheckBox(item);
});
setGraphic(checkBox);
} else {
if (checkBox != null) {
checkBox.setOnAction(null);
}
setGraphic(null);
}
}
};
}
});
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(Res.get("shared.balance"));
column.setMinWidth(100);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionOutputItem, TransactionOutputItem> call(
TableColumn<TransactionOutputItem, TransactionOutputItem> column) {
return new TableCell<>() {
@Override
public void updateItem(TransactionOutputItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(formatter.formatCoinWithCode(item.getTransactionOutput().getValue()));
} else {
setText("");
}
}
};
}
});
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(Res.get("shared.utxo"));
column.setSortable(false);
column.setMinWidth(550);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<TransactionOutputItem, TransactionOutputItem> call(
TableColumn<TransactionOutputItem, TransactionOutputItem> column) {
return new TableCell<>() {
private HyperlinkWithIcon hyperlinkWithIcon;
@Override
public void updateItem(TransactionOutputItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
TransactionOutput transactionOutput = item.getTransactionOutput();
String txId = transactionOutput.getParentTransaction().getTxId().toString();
hyperlinkWithIcon = new ExternalHyperlink(txId + ":" + transactionOutput.getIndex());
hyperlinkWithIcon.setOnAction(event -> GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().txUrl + txId, false));
hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", txId)));
setGraphic(hyperlinkWithIcon);
} else {
if (hyperlinkWithIcon != null) {
hyperlinkWithIcon.setOnAction(null);
}
setGraphic(null);
}
}
};
}
});
tableView.getColumns().add(column);
}
}

View file

@ -1681,11 +1681,14 @@ public class FormBuilder {
public static BalanceTextField addBalanceTextField(GridPane gridPane, int rowIndex, String title) {
return addBalanceTextField(gridPane, rowIndex, title, 20);
}
public static BalanceTextField addBalanceTextField(GridPane gridPane, int rowIndex, String title, double top) {
BalanceTextField balanceTextField = new BalanceTextField(title);
GridPane.setRowIndex(balanceTextField, rowIndex);
GridPane.setColumnIndex(balanceTextField, 0);
GridPane.setMargin(balanceTextField, new Insets(20, 0, 0, 0));
GridPane.setMargin(balanceTextField, new Insets(top, 0, 0, 0));
gridPane.getChildren().add(balanceTextField);
return balanceTextField;

View file

@ -16,6 +16,7 @@ public class JFXInputValidator extends ValidatorBase {
}
public void resetValidation() {
message.set(null);
hasErrors.set(false);
}

View file

@ -21,6 +21,10 @@ public class LengthValidator extends InputValidator {
ValidationResult result = new ValidationResult(true);
int length = (input == null) ? 0 : input.length();
if (this.minLength == this.maxLength) {
if (length != this.minLength)
result = new ValidationResult(false, Res.get("validation.fixedLength", this.minLength));
} else
if (length < this.minLength || length > this.maxLength)
result = new ValidationResult(false, Res.get("validation.length", this.minLength, this.maxLength));

View file

@ -69,5 +69,4 @@ public class LengthValidatorTest {
assertFalse(validator2.validate(null).isValid); // too short
assertFalse(validator2.validate("123456789").isValid); // too long
}
}

View file

@ -70,6 +70,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -251,15 +252,9 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
public void onTorNodeReady() {
socks5ProxyProvider.setSocks5ProxyInternal(networkNode);
boolean seedNodesAvailable = requestDataManager.requestPreliminaryData();
requestDataManager.requestPreliminaryData();
keepAliveManager.start();
p2pServiceListeners.forEach(SetupListener::onTorNodeReady);
if (!seedNodesAvailable) {
isBootstrapped = true;
p2pServiceListeners.forEach(P2PServiceListener::onNoSeedNodeAvailable);
}
}
@Override
@ -315,21 +310,12 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
@Override
public void onUpdatedDataReceived() {
if (!isBootstrapped) {
isBootstrapped = true;
// We don't use a listener at mailboxMessageService as we require the correct
// order of execution. The p2pServiceListeners must be called after
// mailboxMessageService.onUpdatedDataReceived.
mailboxMessageService.onUpdatedDataReceived();
p2pServiceListeners.forEach(P2PServiceListener::onUpdatedDataReceived);
p2PDataStorage.onBootstrapComplete();
}
applyIsBootstrapped(P2PServiceListener::onUpdatedDataReceived);
}
@Override
public void onNoSeedNodeAvailable() {
p2pServiceListeners.forEach(P2PServiceListener::onNoSeedNodeAvailable);
applyIsBootstrapped(P2PServiceListener::onNoSeedNodeAvailable);
}
@Override
@ -342,6 +328,21 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
p2pServiceListeners.forEach(P2PServiceListener::onDataReceived);
}
private void applyIsBootstrapped(Consumer<P2PServiceListener> listenerHandler) {
if (!isBootstrapped) {
isBootstrapped = true;
p2PDataStorage.onBootstrapped();
// Once we have applied the state in the P2P domain we notify our listeners
p2pServiceListeners.forEach(listenerHandler);
// We don't use a listener at mailboxMessageService as we require the correct
// order of execution. The p2pServiceListeners must be called before.
mailboxMessageService.onBootstrapped();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// ConnectionListener implementation

View file

@ -26,11 +26,9 @@ import bisq.network.p2p.SendMailboxMessageListener;
import bisq.network.p2p.messaging.DecryptedMailboxListener;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.NetworkNode;
import bisq.network.p2p.network.SetupListener;
import bisq.network.p2p.peers.BroadcastHandler;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.peers.getdata.RequestDataManager;
import bisq.network.p2p.storage.HashMapChangedListener;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.messages.AddDataMessage;
@ -112,14 +110,12 @@ import static com.google.common.base.Preconditions.checkNotNull;
*/
@Singleton
@Slf4j
public class MailboxMessageService implements SetupListener, HashMapChangedListener,
PersistedDataHost {
public class MailboxMessageService implements HashMapChangedListener, PersistedDataHost {
private static final long REPUBLISH_DELAY_SEC = TimeUnit.MINUTES.toSeconds(2);
private final NetworkNode networkNode;
private final PeerManager peerManager;
private final P2PDataStorage p2PDataStorage;
private final RequestDataManager requestDataManager;
private final EncryptionService encryptionService;
private final IgnoredMailboxService ignoredMailboxService;
private final PersistenceManager<MailboxMessageList> persistenceManager;
@ -137,7 +133,6 @@ public class MailboxMessageService implements SetupListener, HashMapChangedListe
public MailboxMessageService(NetworkNode networkNode,
PeerManager peerManager,
P2PDataStorage p2PDataStorage,
RequestDataManager requestDataManager,
EncryptionService encryptionService,
IgnoredMailboxService ignoredMailboxService,
PersistenceManager<MailboxMessageList> persistenceManager,
@ -147,7 +142,6 @@ public class MailboxMessageService implements SetupListener, HashMapChangedListe
this.networkNode = networkNode;
this.peerManager = peerManager;
this.p2PDataStorage = p2PDataStorage;
this.requestDataManager = requestDataManager;
this.encryptionService = encryptionService;
this.ignoredMailboxService = ignoredMailboxService;
this.persistenceManager = persistenceManager;
@ -155,8 +149,6 @@ public class MailboxMessageService implements SetupListener, HashMapChangedListe
this.clock = clock;
this.republishMailboxEntries = republishMailboxEntries;
this.networkNode.addSetupListener(this);
this.persistenceManager.initialize(mailboxMessageList, PersistenceManager.Source.PRIVATE_LOW_PRIO);
}
@ -226,7 +218,7 @@ public class MailboxMessageService implements SetupListener, HashMapChangedListe
// We don't listen on requestDataManager directly as we require the correct
// order of execution. The p2pService is handling the correct order of execution and we get called
// directly from there.
public void onUpdatedDataReceived() {
public void onBootstrapped() {
if (!isBootstrapped) {
isBootstrapped = true;
// Only now we start listening and processing. The p2PDataStorage is our cache for data we have received
@ -236,6 +228,7 @@ public class MailboxMessageService implements SetupListener, HashMapChangedListe
}
}
public void sendEncryptedMailboxMessage(NodeAddress peer,
PubKeyRing peersPubKeyRing,
MailboxMessage mailboxMessage,
@ -343,25 +336,6 @@ public class MailboxMessageService implements SetupListener, HashMapChangedListe
}
///////////////////////////////////////////////////////////////////////////////////////////
// SetupListener implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onTorNodeReady() {
boolean seedNodesAvailable = requestDataManager.requestPreliminaryData();
if (!seedNodesAvailable) {
isBootstrapped = true;
// As we do not expect a updated data request response we start here with addHashMapChangedListenerAndApply
addHashMapChangedListenerAndApply();
maybeRepublishMailBoxMessages();
}
}
@Override
public void onHiddenServicePublished() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// HashMapChangedListener implementation for ProtectedStorageEntry items
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -155,7 +155,7 @@ public class RequestDataManager implements MessageListener, ConnectionListener,
this.listener = listener;
}
public boolean requestPreliminaryData() {
public void requestPreliminaryData() {
ArrayList<NodeAddress> nodeAddresses = new ArrayList<>(seedNodeAddresses);
if (!nodeAddresses.isEmpty()) {
ArrayList<NodeAddress> finalNodeAddresses = new ArrayList<>(nodeAddresses);
@ -169,9 +169,8 @@ public class RequestDataManager implements MessageListener, ConnectionListener,
}
isPreliminaryDataRequest = true;
return true;
} else {
return false;
checkNotNull(listener).onNoSeedNodeAvailable();
}
}

View file

@ -561,7 +561,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers
}
}
public void onBootstrapComplete() {
public void onBootstrapped() {
removeExpiredEntriesTimer = UserThread.runPeriodically(this::removeExpiredEntries, CHECK_TTL_INTERVAL_SEC);
}

View file

@ -17,12 +17,15 @@
package bisq.price;
import bisq.common.UserThread;
import org.springframework.context.SmartLifecycle;
import java.time.Duration;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.slf4j.Logger;
@ -45,17 +48,19 @@ public abstract class PriceProvider<T> implements SmartLifecycle, Supplier<T> {
@Override
public final T get() {
if (!isRunning())
throw new IllegalStateException("call start() before calling get()");
return cachedResult;
}
@Override
public final void start() {
// we call refresh outside the context of a timer once at startup to ensure that
// any exceptions thrown get propagated and cause the application to halt
refresh();
// do the initial refresh asynchronously
UserThread.runAfter(() -> {
try {
refresh();
} catch (Throwable t) {
log.warn("initial refresh failed", t);
}
}, 1, TimeUnit.MILLISECONDS);
timer.scheduleAtFixedRate(new TimerTask() {
@Override

View file

@ -27,6 +27,9 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* High-level mining {@link FeeRate} operations.
*/
@ -34,6 +37,7 @@ import java.util.concurrent.atomic.AtomicLong;
class FeeRateService {
private final List<FeeRateProvider> providers;
protected final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* Construct a {@link FeeRateService} with a list of all {@link FeeRateProvider}
@ -56,6 +60,10 @@ class FeeRateService {
// Process each provider, retrieve and store their fee rate
providers.forEach(p -> {
FeeRate feeRate = p.get();
if (feeRate == null) {
log.warn("feeRate is null, provider={} ", p.toString());
return;
}
String currency = feeRate.getCurrency();
if ("BTC".equals(currency)) {
sumOfAllFeeRates.getAndAdd(feeRate.getPrice());

View file

@ -61,6 +61,8 @@ class ExchangeRateService {
Map<String, ExchangeRate> aggregateExchangeRates = getAggregateExchangeRates();
providers.forEach(p -> {
if (p.get() == null)
return;
Set<ExchangeRate> exchangeRates = p.get();
// Specific metadata fields for specific providers are expected by the client,
@ -136,6 +138,8 @@ class ExchangeRateService {
private Map<String, List<ExchangeRate>> getCurrencyCodeToExchangeRates() {
Map<String, List<ExchangeRate>> currencyCodeToExchangeRates = new HashMap<>();
for (ExchangeRateProvider p : providers) {
if (p.get() == null)
continue;
for (ExchangeRate exchangeRate : p.get()) {
String currencyCode = exchangeRate.getCurrency();
if (currencyCodeToExchangeRates.containsKey(currencyCode)) {

View file

@ -28,6 +28,7 @@ import java.time.Instant;
import org.junit.jupiter.api.Test;
import static java.lang.Thread.sleep;
import static org.junit.Assert.assertTrue;
/**
@ -66,6 +67,9 @@ public class MempoolFeeRateProviderTest {
// Initialize provider
dummyProvider.start();
try {
sleep(1000);
} catch (InterruptedException e) { }
dummyProvider.stop();
return dummyProvider;
@ -86,6 +90,9 @@ public class MempoolFeeRateProviderTest {
// Initialize provider
dummyProvider.start();
try {
sleep(1000);
} catch (InterruptedException e) { }
dummyProvider.stop();
return dummyProvider;

View file

@ -44,6 +44,7 @@ import ch.qos.logback.core.read.ListAppender;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static java.lang.Thread.sleep;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -287,6 +288,9 @@ public class ExchangeRateServiceTest {
// Initialize provider
dummyProvider.start();
try {
sleep(1000);
} catch (InterruptedException e) { }
dummyProvider.stop();
return dummyProvider;
@ -322,6 +326,9 @@ public class ExchangeRateServiceTest {
// Initialize provider
dummyProvider.start();
try {
sleep(1000);
} catch (InterruptedException e) { }
dummyProvider.stop();
return dummyProvider;