fixes from congestion testing

- refactor main wallet polling
- restart main wallet if connection changes before initial sync
- use cached wallet state throughout app
- avoid rescanning spent outputs until payout tx expected
- allow payment sent/received buttons to be clicked until arrived
- apply timeout to payment sent/received buttons
- load DepositView asynchronously
- remove separate timeout from OpenOffer
- tolerate error importing multisig hex until necessary
This commit is contained in:
woodser 2024-04-18 14:02:22 -04:00
parent 9cbf042da2
commit ca2d7704ab
22 changed files with 802 additions and 680 deletions

View File

@ -112,16 +112,6 @@ monerod3-local:
--fixed-difficulty 500 \
--disable-rpc-ban \
funding-wallet-stagenet:
./.localnet/monero-wallet-rpc \
--stagenet \
--rpc-bind-port 18084 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \
--daemon-ssl-allow-any-cert \
--daemon-address http://127.0.0.1:38081 \
#--proxy 127.0.0.1:49775 \
funding-wallet-local:
@ -133,6 +123,23 @@ funding-wallet-local:
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \
funding-wallet-stagenet:
./.localnet/monero-wallet-rpc \
--stagenet \
--rpc-bind-port 38084 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \
--daemon-ssl-allow-any-cert \
--daemon-address http://127.0.0.1:38081 \
funding-wallet-mainnet:
./.localnet/monero-wallet-rpc \
--rpc-bind-port 18084 \
--rpc-login rpc_user:abc123 \
--rpc-access-control-origins http://localhost:8080 \
--wallet-dir ./.localnet \
# use .bat extension for windows binaries
APP_EXT :=
ifeq ($(OS),Windows_NT)

View File

@ -607,7 +607,7 @@ public final class XmrConnectionService {
long targetHeight = lastInfo.getTargetHeight();
long blocksLeft = targetHeight - lastInfo.getHeight();
if (syncStartHeight == null) syncStartHeight = lastInfo.getHeight();
double percent = targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight)) * 100d; // grant at least 1 block to show progress
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress
downloadListener.progress(percent, blocksLeft, null);
}

View File

@ -116,18 +116,16 @@ public class OfferBookService {
@Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
listener.onAdded(offer);
}
});
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
}
}
});
}
@ -135,18 +133,16 @@ public class OfferBookService {
@Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
listener.onRemoved(offer);
}
});
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
}
}
});
}

View File

@ -34,8 +34,6 @@
package haveno.core.offer;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.proto.ProtoUtil;
import haveno.core.trade.Tradable;
import javafx.beans.property.ObjectProperty;
@ -55,9 +53,6 @@ import java.util.Optional;
@EqualsAndHashCode
@Slf4j
public final class OpenOffer implements Tradable {
// Timeout for offer reservation during takeoffer process. If deposit tx is not completed in that time we reset the offer to AVAILABLE state.
private static final long TIMEOUT = 60;
transient private Timer timeoutTimer;
public enum State {
SCHEDULED,
@ -227,13 +222,6 @@ public final class OpenOffer implements Tradable {
public void setState(State state) {
this.state = state;
stateProperty.set(state);
// We keep it reserved for a limited time, if trade preparation fails we revert to available state
if (this.state == State.RESERVED) { // TODO (woodser): remove this?
startTimeout();
} else {
stopTimeout();
}
}
public ReadOnlyObjectProperty<State> stateProperty() {
@ -252,26 +240,6 @@ public final class OpenOffer implements Tradable {
return state == State.DEACTIVATED;
}
private void startTimeout() {
stopTimeout();
timeoutTimer = UserThread.runAfter(() -> {
log.debug("Timeout for resetting State.RESERVED reached");
if (state == State.RESERVED) {
// we do not need to persist that as at startup any RESERVED state would be reset to AVAILABLE anyway
setState(State.AVAILABLE);
}
}, TIMEOUT);
}
private void stopTimeout() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
@Override
public String toString() {
return "OpenOffer{" +

View File

@ -971,8 +971,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// return if awaiting scheduled tx
if (openOffer.getScheduledTxHashes() != null) return null;
// cache all transactions including from pool
List<MoneroTxWallet> allTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIncludeOutputs(true));
// get all transactions including from pool
List<MoneroTxWallet> allTxs = xmrWalletService.getTransactions(false);
if (preferredSubaddressIndex != null) {

View File

@ -766,6 +766,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
trade.getSelf().getUpdatedMultisigHex(),
receiver.getUnsignedPayoutTxHex(), // include dispute payout tx if arbitrator has their updated multisig info
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
receiverPeer.setDisputeClosedMessage(disputeClosedMessage);
// send dispute closed message
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",

View File

@ -413,7 +413,7 @@ public abstract class Trade implements Tradable, Model {
transient private Subscription tradeStateSubscription;
transient private Subscription tradePhaseSubscription;
transient private Subscription payoutStateSubscription;
transient private TaskLooper txPollLooper;
transient private TaskLooper pollLooper;
transient private Long walletRefreshPeriodMs;
transient private Long syncNormalStartTimeMs;
@ -890,6 +890,10 @@ public abstract class Trade implements Tradable, Model {
public void saveWallet() {
synchronized (walletLock) {
if (!walletExists()) {
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId());
return;
}
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId());
xmrWalletService.saveWallet(wallet);
maybeBackupWallet();
@ -1195,7 +1199,7 @@ public abstract class Trade implements Tradable, Model {
return trader.getDepositTx();
} catch (MoneroError e) {
log.error("Error getting {} deposit tx {}: {}", getPeerRole(trader), depositId, e.getMessage()); // TODO: peer.getRole()
return null;
throw e;
}
}
@ -1264,9 +1268,12 @@ public abstract class Trade implements Tradable, Model {
// TODO: clear other process data
setPayoutTxHex(null);
for (TradePeer peer : getPeers()) {
for (TradePeer peer : getAllTradeParties()) {
peer.setUnsignedPayoutTxHex(null);
peer.setUpdatedMultisigHex(null);
peer.setDisputeClosedMessage(null);
peer.setPaymentSentMessage(null);
peer.setPaymentReceivedMessage(null);
}
}
@ -1597,11 +1604,16 @@ public abstract class Trade implements Tradable, Model {
}
private List<TradePeer> getPeers() {
List<TradePeer> peers = getAllTradeParties();
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers;
}
private List<TradePeer> getAllTradeParties() {
List<TradePeer> peers = new ArrayList<TradePeer>();
peers.add(getMaker());
peers.add(getTaker());
peers.add(getArbitrator());
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers;
}
@ -1801,6 +1813,12 @@ public abstract class Trade implements Tradable, Model {
return (isSeller() ? getBuyer() : getSeller()).getPaymentReceivedMessage() != null; // seller stores message to buyer and arbitrator, peers store message from seller
}
public boolean hasDisputeClosedMessage() {
// arbitrator stores message to buyer and seller, peers store message from arbitrator
return isArbitrator() ? getBuyer().getDisputeClosedMessage() != null || getSeller().getDisputeClosedMessage() != null : getArbitrator().getDisputeClosedMessage() != null;
}
public boolean isPaymentReceived() {
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal();
}
@ -1883,7 +1901,7 @@ public abstract class Trade implements Tradable, Model {
public BigInteger getFrozenAmount() {
BigInteger sum = BigInteger.ZERO;
for (String keyImage : getSelf().getReserveTxKeyImages()) {
List<MoneroOutputWallet> outputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); // TODO: will this check tx pool? avoid
List<MoneroOutputWallet> outputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
}
return sum;
@ -2077,23 +2095,23 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) {
if (isShutDownStarted || isPollInProgress()) return;
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
txPollLooper = new TaskLooper(() -> pollWallet());
txPollLooper.start(walletRefreshPeriodMs);
pollLooper = new TaskLooper(() -> pollWallet());
pollLooper.start(walletRefreshPeriodMs);
}
}
private void stopPolling() {
synchronized (walletLock) {
if (isPollInProgress()) {
txPollLooper.stop();
txPollLooper = null;
pollLooper.stop();
pollLooper = null;
}
}
}
private boolean isPollInProgress() {
synchronized (walletLock) {
return txPollLooper != null;
return pollLooper != null;
}
}
@ -2117,8 +2135,14 @@ public abstract class Trade implements Tradable, Model {
// skip if payout unlocked
if (isPayoutUnlocked()) return;
// rescan spent outputs to detect payout tx after deposits unlocked
if (isDepositsUnlocked() && !isPayoutPublished()) wallet.rescanSpent();
// rescan spent outputs to detect unconfirmed payout tx after payment received message
if (!isPayoutPublished() && (hasPaymentReceivedMessage() || hasDisputeClosedMessage())) {
try {
wallet.rescanSpent();
} catch (Exception e) {
log.warn("Error rescanning spent outputs to detect payout tx for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
}
}
// get txs from trade wallet
boolean payoutExpected = isPaymentReceived() || getSeller().getPaymentReceivedMessage() != null || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal() || getArbitrator().getDisputeClosedMessage() != null;
@ -2129,7 +2153,7 @@ public abstract class Trade implements Tradable, Model {
// warn on double spend // TODO: other handling?
for (MoneroTxWallet tx : txs) {
if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getId());
if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getShortId());
}
// check deposit txs
@ -2189,9 +2213,8 @@ public abstract class Trade implements Tradable, Model {
if (isConnectionRefused) forceRestartTradeWallet();
else {
boolean isWalletConnected = isWalletConnectedToDaemon();
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected
if (!isShutDownStarted && wallet != null && isWalletConnected) {
log.warn("Error polling trade wallet for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
//e.printStackTrace();
}
}

View File

@ -1287,7 +1287,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
private void removeTradeOnError(Trade trade) {
log.warn("TradeManager.removeTradeOnError() tradeId={}, state={}", trade.getId(), trade.getState());
log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState());
synchronized (tradableList) {
// unreserve taker key images

View File

@ -134,14 +134,16 @@ public class BuyerProtocol extends DisputeProtocol {
BuyerSendPaymentSentMessageToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
stopTimeout();
this.errorMessageHandler = null;
resultHandler.handleResult();
handleTaskRunnerSuccess(event);
},
(errorMessage) -> {
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT))
}))
.withTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS))
.run(() -> trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT))
.executeTasks(true);
} catch (Exception e) {
errorMessageHandler.handleErrorMessage("Error confirming payment sent: " + e.getMessage());

View File

@ -131,13 +131,15 @@ public class SellerProtocol extends DisputeProtocol {
SellerSendPaymentReceivedMessageToBuyer.class,
SellerSendPaymentReceivedMessageToArbitrator.class)
.using(new TradeTaskRunner(trade, () -> {
stopTimeout();
this.errorMessageHandler = null;
handleTaskRunnerSuccess(event);
resultHandler.handleResult();
}, (errorMessage) -> {
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT))
}))
.withTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS))
.run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT))
.executeTasks(true);
} catch (Exception e) {
errorMessageHandler.handleErrorMessage("Error confirming payment received: " + e.getMessage());

View File

@ -61,13 +61,17 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
}
processModel.getTradeManager().requestPersistence(); // in case importing multisig hex fails
// import multisig hex
trade.importMultisigHex();
// persist and complete
// persist
processModel.getTradeManager().requestPersistence();
// try to import multisig hex (retry later)
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
complete();
} catch (Throwable t) {
failed(t);

View File

@ -95,7 +95,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
}
trade.requestPersistence();
// process payout tx unless already unlocked
if (!trade.isPayoutUnlocked()) processPayoutTx(message);

View File

@ -61,8 +61,12 @@ public class ProcessPaymentSentMessage extends TradeTask {
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
trade.requestPersistence();
// import multisig hex
trade.importMultisigHex();
// try to import multisig hex (retry later)
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
// update state
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);

View File

@ -1,71 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.protocol.tasks;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SellerPublishDepositTx extends TradeTask {
public SellerPublishDepositTx(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
throw new RuntimeException("SellerPublishesDepositTx not implemented for xmr");
// final Transaction depositTx = processModel.getDepositTx();
// processModel.getTradeWalletService().broadcastTx(depositTx,
// new TxBroadcaster.Callback() {
// @Override
// public void onSuccess(Transaction transaction) {
// if (!completed) {
// // Now as we have published the deposit tx we set it in trade
// trade.applyDepositTx(depositTx);
//
// trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX);
//
// processModel.getBtcWalletService().swapAddressEntryToAvailable(processModel.getOffer().getId(),
// AddressEntry.Context.RESERVED_FOR_TRADE);
//
// processModel.getTradeManager().requestPersistence();
//
// complete();
// } else {
// log.warn("We got the onSuccess callback called after the timeout has been triggered a complete().");
// }
// }
//
// @Override
// public void onFailure(TxBroadcastException exception) {
// if (!completed) {
// failed(exception);
// } else {
// log.warn("We got the onFailure callback called after the timeout has been triggered a complete().");
// }
// }
// });
} catch (Throwable t) {
failed(t);
}
}
}

View File

@ -140,7 +140,7 @@ public class Balances {
// calculate reserved offer balance
reservedOfferBalance = BigInteger.ZERO;
if (xmrWalletService.getWallet() != null) {
List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false));
List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false));
for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount());
}
for (Trade trade : trades) {

View File

@ -11,7 +11,7 @@ public class DownloadListener {
private final DoubleProperty percentage = new SimpleDoubleProperty(-1);
public void progress(double percentage, long blocksLeft, Date date) {
UserThread.await(() -> this.percentage.set(percentage / 100d));
UserThread.await(() -> this.percentage.set(percentage));
}
public void doneDownload() {

View File

@ -33,7 +33,6 @@ import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage;
import haveno.core.trade.protocol.tasks.RemoveOffer;
import haveno.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
import haveno.core.trade.protocol.tasks.SellerPublishDepositTx;
import haveno.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import haveno.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
@ -87,7 +86,6 @@ public class DebugView extends InitializableView<GridPane, Void> {
VerifyPeersAccountAgeWitness.class,
//SellerSendsDepositTxAndDelayedPayoutTxMessage.class,
SellerPublishDepositTx.class,
SellerPublishTradeStatistics.class,
ProcessPaymentSentMessage.class,
@ -140,7 +138,6 @@ public class DebugView extends InitializableView<GridPane, Void> {
RemoveOffer.class,
//SellerSendsDepositTxAndDelayedPayoutTxMessage.class,
SellerPublishDepositTx.class,
SellerPublishTradeStatistics.class,
ProcessPaymentSentMessage.class,

View File

@ -36,6 +36,8 @@ package haveno.desktop.main.funds.deposit;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.common.ThreadUtils;
import haveno.common.UserThread;
import haveno.common.app.DevEnv;
import haveno.common.util.Tuple3;
@ -78,6 +80,7 @@ import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
@ -111,6 +114,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
private Button generateNewAddressButton;
private TitledGroupBg titledGroupBg;
private InputTextField amountTextField;
private static final String THREAD_ID = DepositView.class.getName();
private final XmrWalletService xmrWalletService;
private final Preferences preferences;
@ -146,142 +150,155 @@ public class DepositView extends ActivatableView<VBox, Void> {
confirmationsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations")));
usageColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.usage")));
// trigger creation of at least 1 address
try {
xmrWalletService.getFreshAddressEntry();
} catch (Exception e) {
log.warn("Failed to get wallet txs to initialize DepositView");
e.printStackTrace();
}
// set loading placeholder
Label placeholderLabel = new Label("Loading...");
tableView.setPlaceholder(placeholderLabel);
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses")));
tableViewSelectionListener = (observableValue, oldValue, newValue) -> {
if (newValue != null) {
fillForm(newValue.getAddressString());
GUIUtil.requestFocus(amountTextField);
ThreadUtils.execute(() -> {
// trigger creation of at least 1 address
try {
xmrWalletService.getFreshAddressEntry();
} catch (Exception e) {
log.warn("Failed to create fresh address entry to initialize DepositView");
e.printStackTrace();
}
};
setAddressColumnCellFactory();
setBalanceColumnCellFactory();
setUsageColumnCellFactory();
setConfidenceColumnCellFactory();
addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString));
balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI));
confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed()));
usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage));
tableView.getSortOrder().add(usageColumn);
tableView.setItems(sortedList);
titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet"));
titledGroupBg.getStyleClass().add("last");
qrCodeImageView = new ImageView();
qrCodeImageView.setFitHeight(150);
qrCodeImageView.setFitWidth(150);
qrCodeImageView.getStyleClass().add("qr-code");
Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow")));
qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter(
() -> new QRCodeWindow(getPaymentUri()).show(),
200, TimeUnit.MILLISECONDS));
GridPane.setRowIndex(qrCodeImageView, gridRow);
GridPane.setRowSpan(qrCodeImageView, 4);
GridPane.setColumnIndex(qrCodeImageView, 1);
GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10));
gridPane.getChildren().add(qrCodeImageView);
addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE);
addressTextField.setPaymentLabel(paymentLabelString);
amountTextField = addInputTextField(gridPane, ++gridRow, Res.get("funds.deposit.amount"));
amountTextField.setMaxWidth(380);
if (DevEnv.isDevMode())
amountTextField.setText("10");
titledGroupBg.setVisible(false);
titledGroupBg.setManaged(false);
qrCodeImageView.setVisible(false);
qrCodeImageView.setManaged(false);
addressTextField.setVisible(false);
addressTextField.setManaged(false);
amountTextField.setManaged(false);
Tuple3<Button, CheckBox, HBox> buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow,
Res.get("funds.deposit.generateAddress"),
null,
15);
buttonCheckBoxHBox.third.setSpacing(25);
generateNewAddressButton = buttonCheckBoxHBox.first;
generateNewAddressButton.setOnAction(event -> {
boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty();
if (hasUnusedAddress) {
new Popup().warning(Res.get("funds.deposit.selectUnused")).show();
} else {
XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry();
updateList();
UserThread.execute(() -> {
observableList.stream()
.filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString()))
.findAny()
.ifPresent(depositListItem -> tableView.getSelectionModel().select(depositListItem));
UserThread.execute(() -> {
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses")));
tableViewSelectionListener = (observableValue, oldValue, newValue) -> {
if (newValue != null) {
fillForm(newValue.getAddressString());
GUIUtil.requestFocus(amountTextField);
}
};
setAddressColumnCellFactory();
setBalanceColumnCellFactory();
setUsageColumnCellFactory();
setConfidenceColumnCellFactory();
addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString));
balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI));
confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed()));
usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage));
tableView.getSortOrder().add(usageColumn);
tableView.setItems(sortedList);
titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet"));
titledGroupBg.getStyleClass().add("last");
qrCodeImageView = new ImageView();
qrCodeImageView.setFitHeight(150);
qrCodeImageView.setFitWidth(150);
qrCodeImageView.getStyleClass().add("qr-code");
Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow")));
qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter(
() -> new QRCodeWindow(getPaymentUri()).show(),
200, TimeUnit.MILLISECONDS));
GridPane.setRowIndex(qrCodeImageView, gridRow);
GridPane.setRowSpan(qrCodeImageView, 4);
GridPane.setColumnIndex(qrCodeImageView, 1);
GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10));
gridPane.getChildren().add(qrCodeImageView);
addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE);
addressTextField.setPaymentLabel(paymentLabelString);
amountTextField = addInputTextField(gridPane, ++gridRow, Res.get("funds.deposit.amount"));
amountTextField.setMaxWidth(380);
if (DevEnv.isDevMode())
amountTextField.setText("10");
titledGroupBg.setVisible(false);
titledGroupBg.setManaged(false);
qrCodeImageView.setVisible(false);
qrCodeImageView.setManaged(false);
addressTextField.setVisible(false);
addressTextField.setManaged(false);
amountTextField.setManaged(false);
Tuple3<Button, CheckBox, HBox> buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow,
Res.get("funds.deposit.generateAddress"),
null,
15);
buttonCheckBoxHBox.third.setSpacing(25);
generateNewAddressButton = buttonCheckBoxHBox.first;
generateNewAddressButton.setOnAction(event -> {
boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty();
if (hasUnusedAddress) {
new Popup().warning(Res.get("funds.deposit.selectUnused")).show();
} else {
XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry();
updateList();
UserThread.execute(() -> {
observableList.stream()
.filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString()))
.findAny()
.ifPresent(depositListItem -> tableView.getSelectionModel().select(depositListItem));
});
}
});
}
});
balanceListener = new XmrBalanceListener() {
@Override
public void onBalanceChanged(BigInteger balance) {
updateList();
}
};
walletListener = new MoneroWalletListener() {
@Override
public void onNewBlock(long height) {
updateList();
}
};
GUIUtil.focusWhenAddedToScene(amountTextField);
balanceListener = new XmrBalanceListener() {
@Override
public void onBalanceChanged(BigInteger balance) {
updateList();
}
};
walletListener = new MoneroWalletListener() {
@Override
public void onNewBlock(long height) {
updateList();
}
};
GUIUtil.focusWhenAddedToScene(amountTextField);
});
}, THREAD_ID);
}
@Override
protected void activate() {
tableView.getSelectionModel().selectedItemProperty().addListener(tableViewSelectionListener);
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
// try to update deposits list
try {
updateList();
} catch (Exception e) {
log.warn("Could not update deposits list");
e.printStackTrace();
}
xmrWalletService.addBalanceListener(balanceListener);
xmrWalletService.addWalletListener(walletListener);
amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> {
addressTextField.setAmount(HavenoUtils.parseXmr(t));
updateQRCode();
});
if (tableView.getSelectionModel().getSelectedItem() == null && !sortedList.isEmpty())
tableView.getSelectionModel().select(0);
ThreadUtils.execute(() -> {
UserThread.execute(() -> {
tableView.getSelectionModel().selectedItemProperty().addListener(tableViewSelectionListener);
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
// try to update deposits list
try {
updateList();
} catch (Exception e) {
log.warn("Could not update deposits list");
e.printStackTrace();
}
xmrWalletService.addBalanceListener(balanceListener);
xmrWalletService.addWalletListener(walletListener);
amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> {
addressTextField.setAmount(HavenoUtils.parseXmr(t));
updateQRCode();
});
if (tableView.getSelectionModel().getSelectedItem() == null && !sortedList.isEmpty())
tableView.getSelectionModel().select(0);
});
}, THREAD_ID);
}
@Override
protected void deactivate() {
tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener);
sortedList.comparatorProperty().unbind();
observableList.forEach(DepositListItem::cleanup);
xmrWalletService.removeBalanceListener(balanceListener);
xmrWalletService.removeWalletListener(walletListener);
amountTextFieldSubscription.unsubscribe();
ThreadUtils.execute(() -> {
tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener);
sortedList.comparatorProperty().unbind();
observableList.forEach(DepositListItem::cleanup);
xmrWalletService.removeBalanceListener(balanceListener);
xmrWalletService.removeWalletListener(walletListener);
amountTextFieldSubscription.unsubscribe();
}, THREAD_ID);
}

View File

@ -156,7 +156,6 @@ public class BuyerStep2View extends TradeStepView {
statusLabel.setText(Res.get("shared.preparingConfirmation"));
break;
case BUYER_SENT_PAYMENT_SENT_MSG:
case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG:
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
timeoutTimer = UserThread.runAfter(() -> {
@ -168,6 +167,7 @@ public class BuyerStep2View extends TradeStepView {
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
break;
case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG:
case SELLER_RECEIVED_PAYMENT_SENT_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived"));
@ -442,7 +442,8 @@ public class BuyerStep2View extends TradeStepView {
private boolean confirmPaymentSentPermitted() {
if (!trade.confirmPermitted()) return false;
return trade.isDepositsUnlocked() && trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal();
if (trade.getState() == Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG) return false;
return trade.isDepositsUnlocked() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal();
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -123,20 +123,19 @@ public class SellerStep3View extends TradeStepView {
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
timeoutTimer = UserThread.runAfter(() -> {
busyAnimation.stop();
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
}, 30);
break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived"));
break;
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived"));
break;
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
// We get a popup and the trade closed, so we dont need to show anything here
busyAnimation.stop();
@ -290,7 +289,8 @@ public class SellerStep3View extends TradeStepView {
private boolean confirmPaymentReceivedPermitted() {
if (!trade.confirmPermitted()) return false;
return trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal();
if (trade.getState() == Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG) return false;
return trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() <= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal();
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -116,6 +116,9 @@ First rebuild Haveno: `make skip-tests`.
Run `make arbitrator-desktop` to run an arbitrator on Monero's mainnet or `make arbitrator-desktop-stagenet` to run an arbitrator on Monero's stagenet.
> **Note**
> Unregister the arbitrator before retiring the app, or clients will continue to try to connect for some time.
The Haveno GUI will open. If on mainnet, ignore the error about not receiving a filter object which is not added yet. Click on the `Account` tab and then press `ctrl + r`. A prompt will open asking to enter the key to register the arbitrator. Use a key generated in the previous steps and complete the registration. The arbitrator is now registered and ready to accept requests of dispute resolution.
Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended.