mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-26 20:30:55 +01:00
Merge branch 'master' into 02-scripted-bot-test
This commit is contained in:
commit
b341bb6e89
23 changed files with 579 additions and 119 deletions
|
@ -73,6 +73,6 @@ public enum BaseCurrencyNetwork {
|
|||
}
|
||||
|
||||
public long getDefaultMinFeePerVbyte() {
|
||||
return 2;
|
||||
return 15; // 2021-02-22 due to mempool congestion, increased from 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -16,6 +16,7 @@ public class JFXInputValidator extends ValidatorBase {
|
|||
}
|
||||
|
||||
public void resetValidation() {
|
||||
message.set(null);
|
||||
hasErrors.set(false);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -69,5 +69,4 @@ public class LengthValidatorTest {
|
|||
assertFalse(validator2.validate(null).isValid); // too short
|
||||
assertFalse(validator2.validate("123456789").isValid); // too long
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue