mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-10-21 14:56:44 -04:00
handle unexpected errors due to reorgs (#1909)
- show disclaimer until 30 confirmations to send payment - trade period starts at 30 confirmations - do not delete multisig wallet until payout has 60 confirmations - recover from stale multisig state via payment received nacks - fix a bug which re-signs stale payout tx - add handling for failed or missing deposit and payout txs - buyer can process payout tx to main wallet - do not process outdated payment received messages - poll trade wallet on startup without network calls - recover missing wallet data on create and process dispute payout - arbitrator nacks dispute request if payout already published - recover if offer funding tx is invalidated
This commit is contained in:
parent
7fa633273c
commit
35418e5290
60 changed files with 1474 additions and 623 deletions
1
Makefile
1
Makefile
|
|
@ -440,6 +440,7 @@ monerod:
|
|||
./.localnet/monerod \
|
||||
--bootstrap-daemon-address auto \
|
||||
--rpc-access-control-origins http://localhost:8080 \
|
||||
--rpc-max-connections-per-private-ip 100 \
|
||||
|
||||
seednode:
|
||||
./haveno-seednode$(APP_EXT) \
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ public class AccountAgeWitnessUtils {
|
|||
boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) &&
|
||||
!accountAgeWitnessService.peerHasSignedWitness(trade) &&
|
||||
accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount());
|
||||
log.info("AccountSigning debug log: " +
|
||||
log.debug("AccountSigning debug log: " +
|
||||
"\ntradeId: {}" +
|
||||
"\nis buyer: {}" +
|
||||
"\nbuyer account age witness info: {}" +
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ public class CoreDisputesService {
|
|||
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
|
||||
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed
|
||||
log.warn("Payout amount for buyer is more than wallet's balance. Decreasing payout amount from {} to {}",
|
||||
log.warn("Payout amount for buyer is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}",
|
||||
HavenoUtils.formatXmr(disputeResult.getBuyerPayoutAmountBeforeCost()),
|
||||
HavenoUtils.formatXmr(trade.getWallet().getBalance()));
|
||||
disputeResult.setBuyerPayoutAmountBeforeCost(trade.getWallet().getBalance());
|
||||
|
|
@ -254,7 +254,7 @@ public class CoreDisputesService {
|
|||
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
|
||||
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(trade.getWallet().getBalance()) > 0) { // in case peer's deposit transaction is not confirmed
|
||||
log.warn("Payout amount for seller is more than wallet's balance. Decreasing payout amount from {} to {}",
|
||||
log.warn("Payout amount for seller is more than wallet's balance. This can happen if a deposit tx is dropped. Decreasing payout amount from {} to {}",
|
||||
HavenoUtils.formatXmr(disputeResult.getSellerPayoutAmountBeforeCost()),
|
||||
HavenoUtils.formatXmr(trade.getWallet().getBalance()));
|
||||
disputeResult.setSellerPayoutAmountBeforeCost(trade.getWallet().getBalance());
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public final class XmrConnectionService {
|
|||
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
|
||||
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
|
||||
private static final int MAX_CONSECUTIVE_ERRORS = 4; // max errors before switching connections
|
||||
private static final int MAX_CONSECUTIVE_ERRORS = 3; // max errors before switching connections
|
||||
private static int numConsecutiveErrors = 0;
|
||||
|
||||
public enum XmrConnectionFallbackType {
|
||||
|
|
|
|||
|
|
@ -91,11 +91,13 @@ public class TradeInfo implements Payload {
|
|||
private final boolean isDepositsPublished;
|
||||
private final boolean isDepositsConfirmed;
|
||||
private final boolean isDepositsUnlocked;
|
||||
private final boolean isDepositsFinalized;
|
||||
private final boolean isPaymentSent;
|
||||
private final boolean isPaymentReceived;
|
||||
private final boolean isPayoutPublished;
|
||||
private final boolean isPayoutConfirmed;
|
||||
private final boolean isPayoutUnlocked;
|
||||
private final boolean isPayoutFinalized;
|
||||
private final boolean isCompleted;
|
||||
private final String contractAsJson;
|
||||
private final ContractInfo contract;
|
||||
|
|
@ -135,11 +137,13 @@ public class TradeInfo implements Payload {
|
|||
this.isDepositsPublished = builder.isDepositsPublished();
|
||||
this.isDepositsConfirmed = builder.isDepositsConfirmed();
|
||||
this.isDepositsUnlocked = builder.isDepositsUnlocked();
|
||||
this.isDepositsFinalized = builder.isDepositsFinalized();
|
||||
this.isPaymentSent = builder.isPaymentSent();
|
||||
this.isPaymentReceived = builder.isPaymentReceived();
|
||||
this.isPayoutPublished = builder.isPayoutPublished();
|
||||
this.isPayoutConfirmed = builder.isPayoutConfirmed();
|
||||
this.isPayoutUnlocked = builder.isPayoutUnlocked();
|
||||
this.isPayoutFinalized = builder.isPayoutFinalized();
|
||||
this.isCompleted = builder.isCompleted();
|
||||
this.contractAsJson = builder.getContractAsJson();
|
||||
this.contract = builder.getContract();
|
||||
|
|
@ -199,11 +203,13 @@ public class TradeInfo implements Payload {
|
|||
.withIsDepositsPublished(trade.isDepositsPublished())
|
||||
.withIsDepositsConfirmed(trade.isDepositsConfirmed())
|
||||
.withIsDepositsUnlocked(trade.isDepositsUnlocked())
|
||||
.withIsDepositsFinalized(trade.isDepositsFinalized())
|
||||
.withIsPaymentSent(trade.isPaymentSent())
|
||||
.withIsPaymentReceived(trade.isPaymentReceived())
|
||||
.withIsPayoutPublished(trade.isPayoutPublished())
|
||||
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
|
||||
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
|
||||
.withIsPayoutFinalized(trade.isPayoutFinalized())
|
||||
.withIsCompleted(trade.isCompleted())
|
||||
.withContractAsJson(trade.getContractAsJson())
|
||||
.withContract(contractInfo)
|
||||
|
|
@ -252,12 +258,14 @@ public class TradeInfo implements Payload {
|
|||
.setIsDepositsPublished(isDepositsPublished)
|
||||
.setIsDepositsConfirmed(isDepositsConfirmed)
|
||||
.setIsDepositsUnlocked(isDepositsUnlocked)
|
||||
.setIsDepositsFinalized(isDepositsFinalized)
|
||||
.setIsPaymentSent(isPaymentSent)
|
||||
.setIsPaymentReceived(isPaymentReceived)
|
||||
.setIsCompleted(isCompleted)
|
||||
.setIsPayoutPublished(isPayoutPublished)
|
||||
.setIsPayoutConfirmed(isPayoutConfirmed)
|
||||
.setIsPayoutUnlocked(isPayoutUnlocked)
|
||||
.setIsPayoutFinalized(isPayoutFinalized)
|
||||
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
||||
.setContract(contract.toProtoMessage())
|
||||
.setStartTime(startTime)
|
||||
|
|
@ -299,12 +307,14 @@ public class TradeInfo implements Payload {
|
|||
.withIsDepositsPublished(proto.getIsDepositsPublished())
|
||||
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
|
||||
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
|
||||
.withIsDepositsFinalized(proto.getIsDepositsFinalized())
|
||||
.withIsPaymentSent(proto.getIsPaymentSent())
|
||||
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
||||
.withIsCompleted(proto.getIsCompleted())
|
||||
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
||||
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
|
||||
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
|
||||
.withIsPayoutFinalized(proto.getIsPayoutFinalized())
|
||||
.withContractAsJson(proto.getContractAsJson())
|
||||
.withContract((ContractInfo.fromProto(proto.getContract())))
|
||||
.withStartTime(proto.getStartTime())
|
||||
|
|
@ -345,11 +355,13 @@ public class TradeInfo implements Payload {
|
|||
", isDepositsPublished=" + isDepositsPublished + "\n" +
|
||||
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
|
||||
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
|
||||
", isDepositsFinalized=" + isDepositsFinalized + "\n" +
|
||||
", isPaymentSent=" + isPaymentSent + "\n" +
|
||||
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
|
||||
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
|
||||
", isPayoutFinalized=" + isPayoutFinalized + "\n" +
|
||||
", isCompleted=" + isCompleted + "\n" +
|
||||
", offer=" + offer + "\n" +
|
||||
", contractAsJson=" + contractAsJson + "\n" +
|
||||
|
|
|
|||
|
|
@ -64,11 +64,13 @@ public final class TradeInfoV1Builder {
|
|||
private boolean isDepositsPublished;
|
||||
private boolean isDepositsConfirmed;
|
||||
private boolean isDepositsUnlocked;
|
||||
private boolean isDepositsFinalized;
|
||||
private boolean isPaymentSent;
|
||||
private boolean isPaymentReceived;
|
||||
private boolean isPayoutPublished;
|
||||
private boolean isPayoutConfirmed;
|
||||
private boolean isPayoutUnlocked;
|
||||
private boolean isPayoutFinalized;
|
||||
private boolean isCompleted;
|
||||
private String contractAsJson;
|
||||
private ContractInfo contract;
|
||||
|
|
@ -242,6 +244,11 @@ public final class TradeInfoV1Builder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsDepositsFinalized(boolean isDepositsFinalized) {
|
||||
this.isDepositsFinalized = isDepositsFinalized;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsPaymentSent(boolean isPaymentSent) {
|
||||
this.isPaymentSent = isPaymentSent;
|
||||
return this;
|
||||
|
|
@ -267,6 +274,11 @@ public final class TradeInfoV1Builder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) {
|
||||
this.isPayoutFinalized = isPayoutFinalized;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
|
||||
this.isCompleted = isCompleted;
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ public class TradeEvents {
|
|||
case DEPOSITS_PUBLISHED:
|
||||
break;
|
||||
case DEPOSITS_UNLOCKED:
|
||||
case DEPOSITS_FINALIZED: // TODO: use a separate message for deposits finalized?
|
||||
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
|
||||
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1224,17 +1224,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
|
||||
|
||||
// check if split output tx is available for offer
|
||||
if (splitOutputTx.isLocked()) return splitOutputTx;
|
||||
else {
|
||||
boolean isAvailable = true;
|
||||
for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) {
|
||||
if (output.isSpent() || output.isFrozen()) {
|
||||
isAvailable = false;
|
||||
break;
|
||||
if (splitOutputTx != null) {
|
||||
if (splitOutputTx.isLocked()) return splitOutputTx;
|
||||
else {
|
||||
boolean isAvailable = true;
|
||||
for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) {
|
||||
if (output.isSpent() || output.isFrozen()) {
|
||||
isAvailable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
|
||||
else log.warn("Split output tx {} is no longer available for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
|
||||
}
|
||||
if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
|
||||
else log.warn("Split output tx is no longer available for offer {}", openOffer.getId());
|
||||
} else {
|
||||
log.warn("Split output tx {} no longer exists for offer {}", openOffer.getSplitOutputTxHash(), openOffer.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1757,6 +1761,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
errorMessage = "Exception at handleSignOfferRequest " + e.getMessage();
|
||||
log.error(errorMessage + "\n", e);
|
||||
} finally {
|
||||
if (result == false && errorMessage == null) {
|
||||
log.warn("Arbitrator is NACKing SignOfferRequest for unknown reason with offerId={}. That should never happen", request.getOfferId());
|
||||
log.warn("Printing stacktrace:");
|
||||
Thread.dumpStack();
|
||||
}
|
||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage);
|
||||
}
|
||||
}
|
||||
|
|
@ -1946,8 +1955,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
result,
|
||||
errorMessage);
|
||||
|
||||
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
|
||||
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid());
|
||||
if (ackMessage.isSuccess()) {
|
||||
log.info("Send AckMessage for {} to peer {} with offerId {} and sourceUid {}",
|
||||
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid());
|
||||
} else {
|
||||
log.warn("Sending NACK for {} to peer {} with offerId {} and sourceUid {}, errorMessage={}",
|
||||
reqClass.getSimpleName(), sender, offerId, ackMessage.getSourceUid(), errorMessage);
|
||||
}
|
||||
|
||||
p2PService.sendEncryptedDirectMessage(
|
||||
sender,
|
||||
senderPubKeyRing,
|
||||
|
|
|
|||
|
|
@ -154,15 +154,15 @@ public abstract class SupportManager {
|
|||
// Message handler
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected void handleChatMessage(ChatMessage chatMessage) {
|
||||
protected void handle(ChatMessage chatMessage) {
|
||||
final String tradeId = chatMessage.getTradeId();
|
||||
final String uid = chatMessage.getUid();
|
||||
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
|
||||
boolean channelOpen = channelOpen(chatMessage);
|
||||
if (!channelOpen) {
|
||||
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
||||
log.warn("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1);
|
||||
Timer timer = UserThread.runAfter(() -> handle(chatMessage), 1);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
|
||||
|
|
@ -217,7 +217,11 @@ public abstract class SupportManager {
|
|||
synchronized (dispute.getChatMessages()) {
|
||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||
if (trade.getDisputeState().isCloseRequested()) {
|
||||
if (trade.getDisputeState().isRequested()) {
|
||||
log.warn("DisputeOpenedMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||
dispute.setIsClosed();
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
} else if (trade.getDisputeState().isCloseRequested()) {
|
||||
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||
dispute.setIsClosed();
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import haveno.core.trade.ArbitratorTrade;
|
|||
import haveno.core.trade.ClosedTradableManager;
|
||||
import haveno.core.trade.Contract;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.SellerTrade;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.TradeManager;
|
||||
import haveno.core.trade.protocol.TradePeer;
|
||||
|
|
@ -222,7 +223,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// We get this message at both peers. The dispute object is in context of the trader
|
||||
public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage);
|
||||
public abstract void handle(DisputeClosedMessage disputeClosedMessage);
|
||||
|
||||
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
|
||||
|
||||
|
|
@ -403,13 +404,22 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
|
||||
// export multisig hex if needed
|
||||
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
||||
try {
|
||||
trade.exportMultisigHex();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to export multisig hex", e);
|
||||
// try to import latest multisig info
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to import multisig hex", e);
|
||||
}
|
||||
|
||||
// try to export latest multisig info
|
||||
try {
|
||||
trade.exportMultisigHex();
|
||||
if (trade instanceof SellerTrade) {
|
||||
trade.getProcessModel().setPaymentSentPayoutTxStale(true); // exporting multisig hex will invalidate previously unsigned payout txs
|
||||
trade.getSelf().setUnsignedPayoutTxHex(null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to export multisig hex", e);
|
||||
}
|
||||
|
||||
// create dispute opened message
|
||||
|
|
@ -490,7 +500,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
}
|
||||
|
||||
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
|
||||
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
||||
protected void handle(DisputeOpenedMessage message) {
|
||||
Dispute msgDispute = message.getDispute();
|
||||
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId());
|
||||
|
||||
|
|
@ -500,16 +510,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
log.warn("Ignoring DisputeOpenedMessage for trade {} because it does not exist", msgDispute.getTradeId());
|
||||
return;
|
||||
}
|
||||
if (trade.isPayoutPublished()) {
|
||||
log.warn("Ignoring DisputeOpenedMessage for {} {} because payout is already published", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// find existing dispute
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(msgDispute);
|
||||
|
||||
// determine if re-opening dispute
|
||||
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||
boolean reOpen = storedDisputeOptional.isPresent();
|
||||
|
||||
// use existing dispute or create new
|
||||
Dispute dispute = reOpen ? storedDisputeOptional.get() : msgDispute;
|
||||
|
|
@ -588,6 +594,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender;
|
||||
if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex());
|
||||
|
||||
// TODO: peer needs to import multisig hex at some point
|
||||
// TODO: DisputeOpenedMessage should include arbitrator's updated multisig hex too
|
||||
// TODO: arbitrator needs to import multisig info then scan for updated state?
|
||||
|
||||
// arbitrator syncs and polls wallet unless finalized
|
||||
if (trade.isArbitrator() && !trade.isPayoutFinalized()) {
|
||||
trade.syncAndPollWallet();
|
||||
trade.recoverIfMissingWalletData();
|
||||
}
|
||||
|
||||
// nack if payout published
|
||||
if (trade.isPayoutPublished()) {
|
||||
throw new RuntimeException("Ignoring DisputeOpenedMessage because payout is already published for " + trade.getClass().getSimpleName() + " " + trade.getId() + ", payoutTxId=" + trade.getPayoutTxId());
|
||||
}
|
||||
|
||||
// add chat message with price info
|
||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||
|
||||
|
|
@ -934,6 +955,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
// sync and poll
|
||||
trade.syncAndPollWallet();
|
||||
|
||||
// recover if missing wallet data
|
||||
trade.recoverIfMissingWalletData();
|
||||
|
||||
// check if payout tx already published
|
||||
String alreadyPublishedMsg = "Cannot create dispute payout tx because payout tx is already published for trade " + trade.getId();
|
||||
if (trade.isPayoutPublished()) throw new RuntimeException(alreadyPublishedMsg);
|
||||
|
|
|
|||
|
|
@ -148,11 +148,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
|
||||
ThreadUtils.execute(() -> {
|
||||
if (message instanceof DisputeOpenedMessage) {
|
||||
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||
handle((DisputeOpenedMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
handleChatMessage((ChatMessage) message);
|
||||
handle((ChatMessage) message);
|
||||
} else if (message instanceof DisputeClosedMessage) {
|
||||
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||
handle((DisputeClosedMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
|
|
@ -226,11 +226,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
|
||||
// received by both peers when arbitrator closes disputes
|
||||
@Override
|
||||
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
|
||||
handleDisputeClosedMessage(disputeClosedMessage, true);
|
||||
public void handle(DisputeClosedMessage disputeClosedMessage) {
|
||||
handle(disputeClosedMessage, true);
|
||||
}
|
||||
|
||||
private void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage, boolean reprocessOnError) {
|
||||
private void handle(DisputeClosedMessage disputeClosedMessage, boolean reprocessOnError) {
|
||||
|
||||
// get dispute's trade
|
||||
final Trade trade = tradeManager.getTrade(disputeClosedMessage.getTradeId());
|
||||
|
|
@ -261,7 +261,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
"We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
// We delay 2 sec. to be sure the comm. msg gets added first
|
||||
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeClosedMessage), 2);
|
||||
Timer timer = UserThread.runAfter(() -> handle(disputeClosedMessage), 2);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " +
|
||||
|
|
@ -329,13 +329,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
} else {
|
||||
try {
|
||||
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
signAndPublishDisputePayoutTx(trade);
|
||||
processDisputePayoutTx(trade);
|
||||
} catch (Exception e) {
|
||||
|
||||
// check if payout published again
|
||||
trade.syncAndPollWallet();
|
||||
if (trade.isPayoutPublished()) {
|
||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
log.warn("Payout tx already published for {} {}, skipping dispute processing", trade.getClass().getSimpleName(), trade.getId());
|
||||
} else {
|
||||
if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) throw e;
|
||||
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
|
||||
|
|
@ -363,6 +363,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
|
||||
// nack bad message and do not reprocess
|
||||
if (HavenoUtils.isIllegal(e)) {
|
||||
trade.setPayoutTxHex(null); // clear signed payout tx hex
|
||||
trade.getArbitrator().setDisputeClosedMessage(null); // message is processed
|
||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
String warningMsg = "Error processing dispute closed message: " + e.getMessage() + "\n\nOpen another dispute to try again (ctrl+o).";
|
||||
|
|
@ -397,12 +398,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
}
|
||||
|
||||
log.warn("Reprocessing dispute closed message for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
handleDisputeClosedMessage(trade.getArbitrator().getDisputeClosedMessage(), reprocessOnError);
|
||||
handle(trade.getArbitrator().getDisputeClosedMessage(), reprocessOnError);
|
||||
}
|
||||
}, trade.getId());
|
||||
}
|
||||
|
||||
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade) {
|
||||
// TODO: make this handling more consistent with trade.processPayoutTx(), move there?
|
||||
private MoneroTxSet processDisputePayoutTx(Trade trade) {
|
||||
|
||||
// recover if missing wallet data
|
||||
trade.recoverIfMissingWalletData();
|
||||
|
||||
// gather trade info
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
|
|
@ -468,6 +473,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
// sign arbitrator-signed payout tx
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
try {
|
||||
log.info("Signing dispute payout tx for {} {}", getClass().getSimpleName(), trade.getShortId());
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||
|
|
@ -492,6 +498,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
log.info("Dispute payout tx fee is within tolerance for {} {}", getClass().getSimpleName(), trade.getShortId());
|
||||
}
|
||||
} else {
|
||||
log.warn("Payout tx already signed for {} {}, skipping signing", getClass().getSimpleName(), trade.getShortId());
|
||||
disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex());
|
||||
}
|
||||
|
||||
|
|
@ -503,8 +510,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||
if (HavenoUtils.isNotEnoughSigners(e)) throw new IllegalArgumentException(e);
|
||||
if (trade.isPayoutPublished()) return null;
|
||||
if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e) || HavenoUtils.isFailedToParse(e)) throw new IllegalArgumentException(e);
|
||||
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);
|
||||
|
|
|
|||
|
|
@ -101,11 +101,11 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
|
||||
if (message instanceof DisputeOpenedMessage) {
|
||||
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||
handle((DisputeOpenedMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
handleChatMessage((ChatMessage) message);
|
||||
handle((ChatMessage) message);
|
||||
} else if (message instanceof DisputeClosedMessage) {
|
||||
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||
handle((DisputeClosedMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
|
||||
@Override
|
||||
// We get that message at both peers. The dispute object is in context of the trader
|
||||
public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) {
|
||||
public void handle(DisputeClosedMessage disputeResultMessage) {
|
||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||
String tradeId = disputeResult.getTradeId();
|
||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||
|
|
@ -163,7 +163,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
// We delay 2 sec. to be sure the comm. msg gets added first
|
||||
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2);
|
||||
Timer timer = UserThread.runAfter(() -> handle(disputeResultMessage), 2);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
||||
|
|
|
|||
|
|
@ -97,11 +97,11 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
|
||||
if (message instanceof DisputeOpenedMessage) {
|
||||
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||
handle((DisputeOpenedMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
handleChatMessage((ChatMessage) message);
|
||||
handle((ChatMessage) message);
|
||||
} else if (message instanceof DisputeClosedMessage) {
|
||||
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||
handle((DisputeClosedMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
|
||||
@Override
|
||||
// We get that message at both peers. The dispute object is in context of the trader
|
||||
public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) {
|
||||
public void handle(DisputeClosedMessage disputeResultMessage) {
|
||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||
String tradeId = disputeResult.getTradeId();
|
||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||
|
|
@ -162,7 +162,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
// We delay 2 sec. to be sure the comm. msg gets added first
|
||||
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2);
|
||||
Timer timer = UserThread.runAfter(() -> handle(disputeResultMessage), 2);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ public class TraderChatManager extends SupportManager {
|
|||
log.info("Received {} with tradeId {} and uid {}",
|
||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
if (message instanceof ChatMessage) {
|
||||
handleChatMessage((ChatMessage) message);
|
||||
handle((ChatMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import haveno.core.trade.messages.PaymentSentMessage;
|
|||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.util.JsonUtil;
|
||||
import haveno.core.xmr.wallet.XmrWalletBase;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
|
||||
|
|
@ -626,13 +627,17 @@ public class HavenoUtils {
|
|||
}
|
||||
|
||||
public static boolean isUnresponsive(Throwable e) {
|
||||
return isConnectionRefused(e) || isReadTimeout(e);
|
||||
return isConnectionRefused(e) || isReadTimeout(e) || XmrWalletBase.isSyncWithProgressTimeout(e);
|
||||
}
|
||||
|
||||
public static boolean isNotEnoughSigners(Throwable e) {
|
||||
return e != null && e.getMessage().contains("Not enough signers");
|
||||
}
|
||||
|
||||
public static boolean isFailedToParse(Throwable e) {
|
||||
return e != null && e.getMessage().contains("Failed to parse");
|
||||
}
|
||||
|
||||
public static boolean isTransactionRejected(Throwable e) {
|
||||
return e != null && e.getMessage().contains("was rejected");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -459,6 +459,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
return;
|
||||
}
|
||||
|
||||
// add random delay up to 10s to avoid syncing at exactly the same time
|
||||
if (trades.size() > 1 && trade.walletExists()) {
|
||||
int delay = (int) (Math.random() * 10000);
|
||||
HavenoUtils.waitFor(delay);
|
||||
}
|
||||
|
||||
// initialize trade
|
||||
initPersistedTrade(trade);
|
||||
|
||||
|
|
@ -484,7 +490,16 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
|
||||
// sync idle trades once in background after active trades
|
||||
for (Trade trade : trades) {
|
||||
if (trade.isIdling()) ThreadUtils.submitToPool(() -> trade.syncAndPollWallet());
|
||||
if (trade.isIdling()) ThreadUtils.submitToPool(() -> {
|
||||
|
||||
// add random delay up to 10s to avoid syncing at exactly the same time
|
||||
if (trades.size() > 1 && trade.walletExists()) {
|
||||
int delay = (int) (Math.random() * 10000);
|
||||
HavenoUtils.waitFor(delay);
|
||||
}
|
||||
|
||||
trade.syncAndPollWallet();
|
||||
});
|
||||
}
|
||||
|
||||
// process after all wallets initialized
|
||||
|
|
@ -492,7 +507,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
|
||||
// handle uninitialized trades
|
||||
for (Trade trade : uninitializedTrades) {
|
||||
trade.onProtocolError();
|
||||
trade.onProtocolInitializationError();
|
||||
}
|
||||
|
||||
// freeze or thaw outputs
|
||||
|
|
@ -630,7 +645,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
// process with protocol
|
||||
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||
log.warn("Maker error during trade initialization: " + errorMessage);
|
||||
trade.onProtocolError();
|
||||
trade.onProtocolInitializationError();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -724,7 +739,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
// process with protocol
|
||||
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
||||
trade.onProtocolError();
|
||||
trade.onProtocolInitializationError();
|
||||
});
|
||||
|
||||
requestPersistence();
|
||||
|
|
@ -936,7 +951,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
requestPersistence();
|
||||
}, errorMessage -> {
|
||||
log.warn("Taker error during trade initialization: " + errorMessage);
|
||||
trade.onProtocolError();
|
||||
trade.onProtocolInitializationError();
|
||||
xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling
|
||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||
});
|
||||
|
|
@ -1039,16 +1054,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
|
||||
@Override
|
||||
public void onMinuteTick() {
|
||||
updateTradePeriodState();
|
||||
ThreadUtils.submitToPool(() -> updateTradePeriodState()); // update trade period off main thread
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: could use monerod.getBlocksByHeight() to more efficiently update trade period state
|
||||
private void updateTradePeriodState() {
|
||||
if (isShutDownStarted) return;
|
||||
synchronized (tradableList.getList()) {
|
||||
for (Trade trade : tradableList.getList()) {
|
||||
if (!trade.isInitialized() || trade.isPayoutPublished()) continue;
|
||||
for (Trade trade : getOpenTrades()) {
|
||||
if (!trade.isInitialized() || trade.isPayoutPublished()) continue;
|
||||
try {
|
||||
trade.maybeUpdateTradePeriod();
|
||||
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
|
||||
Date halfTradePeriodDate = trade.getHalfTradePeriodDate();
|
||||
if (maxTradePeriodDate != null && halfTradePeriodDate != null) {
|
||||
|
|
@ -1061,6 +1078,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
requestPersistence();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Error updating trade period state for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), e.getMessage(), e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1075,11 +1095,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
public void onMoveInvalidTradeToFailedTrades(Trade trade) {
|
||||
failedTradesManager.add(trade);
|
||||
removeTrade(trade);
|
||||
xmrWalletService.fixReservedOutputs();
|
||||
}
|
||||
|
||||
public void onMoveFailedTradeToPendingTrades(Trade trade) {
|
||||
addTradeToPendingTrades(trade);
|
||||
failedTradesManager.removeTrade(trade);
|
||||
xmrWalletService.fixReservedOutputs();
|
||||
}
|
||||
|
||||
public void onMoveClosedTradeToPendingTrades(Trade trade) {
|
||||
|
|
@ -1224,8 +1246,17 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
updatedMultisigHex);
|
||||
|
||||
// send ack message
|
||||
log.info("Send AckMessage for {} to peer {}. tradeId={}, sourceUid={}",
|
||||
ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid);
|
||||
if (!result) {
|
||||
if (errorMessage == null) {
|
||||
log.warn("Sending NACK for {} to peer {} without error message. That should never happen. tradeId={}, sourceUid={}",
|
||||
ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid);
|
||||
}
|
||||
log.warn("Sending NACK for {} to peer {}. tradeId={}, sourceUid={}, errorMessage={}, updatedMultisigHex={}",
|
||||
ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid, errorMessage, updatedMultisigHex == null ? "null" : updatedMultisigHex.length() + " characters");
|
||||
} else {
|
||||
log.info("Sending AckMessage for {} to peer {}. tradeId={}, sourceUid={}",
|
||||
ackMessage.getSourceMsgClassName(), peer, tradeId, sourceUid);
|
||||
}
|
||||
p2PService.getMailboxMessageService().sendEncryptedMailboxMessage(
|
||||
peer,
|
||||
peersPubKeyRing,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
@Setter
|
||||
@Nullable
|
||||
private byte[] sellerSignature;
|
||||
@Setter
|
||||
@Nullable
|
||||
private String payoutTxId;
|
||||
|
||||
public PaymentReceivedMessage(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
|
|
@ -61,7 +64,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
boolean deferPublishPayout,
|
||||
AccountAgeWitness buyerAccountAgeWitness,
|
||||
@Nullable SignedWitness buyerSignedWitness,
|
||||
@Nullable PaymentSentMessage paymentSentMessage) {
|
||||
@Nullable PaymentSentMessage paymentSentMessage,
|
||||
@Nullable String payoutTxId) {
|
||||
this(tradeId,
|
||||
senderNodeAddress,
|
||||
uid,
|
||||
|
|
@ -72,7 +76,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
deferPublishPayout,
|
||||
buyerAccountAgeWitness,
|
||||
buyerSignedWitness,
|
||||
paymentSentMessage);
|
||||
paymentSentMessage,
|
||||
payoutTxId);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -90,7 +95,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
boolean deferPublishPayout,
|
||||
AccountAgeWitness buyerAccountAgeWitness,
|
||||
@Nullable SignedWitness buyerSignedWitness,
|
||||
PaymentSentMessage paymentSentMessage) {
|
||||
PaymentSentMessage paymentSentMessage,
|
||||
@Nullable String payoutTxId) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
|
||||
|
|
@ -100,6 +106,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
this.paymentSentMessage = paymentSentMessage;
|
||||
this.buyerAccountAgeWitness = buyerAccountAgeWitness;
|
||||
this.buyerSignedWitness = buyerSignedWitness;
|
||||
this.payoutTxId = payoutTxId;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -116,6 +123,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
Optional.ofNullable(buyerSignedWitness).ifPresent(buyerSignedWitness -> builder.setBuyerSignedWitness(buyerSignedWitness.toProtoSignedWitness()));
|
||||
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||
Optional.ofNullable(sellerSignature).ifPresent(e -> builder.setSellerSignature(ByteString.copyFrom(e)));
|
||||
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
|
||||
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +146,8 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
proto.getDeferPublishPayout(),
|
||||
buyerAccountAgeWitness,
|
||||
buyerSignedWitness,
|
||||
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
|
||||
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
|
||||
message.setSellerSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getSellerSignature()));
|
||||
return message;
|
||||
}
|
||||
|
|
@ -154,6 +163,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
|||
",\n deferPublishPayout=" + deferPublishPayout +
|
||||
",\n paymentSentMessage=" + paymentSentMessage +
|
||||
",\n sellerSignature=" + sellerSignature +
|
||||
",\n payoutTxId=" + payoutTxId +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,13 +120,23 @@ public class BuyerProtocol extends DisputeProtocol {
|
|||
|
||||
public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
|
||||
|
||||
// advance trade state
|
||||
if (trade.isDepositsUnlocked() || trade.isDepositsFinalized() || trade.isPaymentSent()) {
|
||||
trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT);
|
||||
} else {
|
||||
errorMessageHandler.handleErrorMessage("Cannot confirm payment sent for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " in state " + trade.getState());
|
||||
return;
|
||||
}
|
||||
|
||||
// process on trade thread
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
|
||||
try {
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT)
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.DEPOSITS_FINALIZED, Trade.Phase.PAYMENT_SENT)
|
||||
.with(event)
|
||||
.preCondition(trade.confirmPermitted()))
|
||||
.setup(tasks(ApplyFilter.class,
|
||||
|
|
@ -145,7 +155,6 @@ public class BuyerProtocol extends DisputeProtocol {
|
|||
trade.setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||
handleTaskRunnerFault(event, errorMessage);
|
||||
})))
|
||||
.run(() -> trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT))
|
||||
.executeTasks(true);
|
||||
} catch (Exception e) {
|
||||
errorMessageHandler.handleErrorMessage("Error confirming payment sent: " + e.getMessage());
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ public abstract class DisputeProtocol extends TradeProtocol {
|
|||
// Trader has not yet received the peer's signature but has clicked the accept button.
|
||||
public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED;
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
expect(anyPhase(
|
||||
Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
Trade.Phase.DEPOSITS_FINALIZED,
|
||||
Trade.Phase.PAYMENT_SENT,
|
||||
Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(event)
|
||||
|
|
@ -107,7 +109,9 @@ public abstract class DisputeProtocol extends TradeProtocol {
|
|||
// Trader has already received the peer's signature and has clicked the accept button as well.
|
||||
public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED;
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
expect(anyPhase(
|
||||
Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
Trade.Phase.DEPOSITS_FINALIZED,
|
||||
Trade.Phase.PAYMENT_SENT,
|
||||
Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(event)
|
||||
|
|
@ -135,7 +139,9 @@ public abstract class DisputeProtocol extends TradeProtocol {
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) {
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
expect(anyPhase(
|
||||
Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
Trade.Phase.DEPOSITS_FINALIZED,
|
||||
Trade.Phase.PAYMENT_SENT,
|
||||
Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(message)
|
||||
|
|
@ -145,7 +151,9 @@ public abstract class DisputeProtocol extends TradeProtocol {
|
|||
}
|
||||
|
||||
protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) {
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
expect(anyPhase(
|
||||
Trade.Phase.DEPOSITS_UNLOCKED,
|
||||
Trade.Phase.DEPOSITS_FINALIZED,
|
||||
Trade.Phase.PAYMENT_SENT,
|
||||
Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(message)
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||
@Getter
|
||||
@Setter
|
||||
private boolean importMultisigHexScheduled;
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean paymentSentPayoutTxStale;
|
||||
private ObjectProperty<Boolean> paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false);
|
||||
@Deprecated
|
||||
private ObjectProperty<MessageState> paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
||||
|
|
@ -237,7 +240,8 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
|
||||
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
|
||||
.setTradeProtocolErrorHeight(tradeProtocolErrorHeight)
|
||||
.setImportMultisigHexScheduled(importMultisigHexScheduled);
|
||||
.setImportMultisigHexScheduled(importMultisigHexScheduled)
|
||||
.setPaymentSentPayoutTxStale(paymentSentPayoutTxStale);
|
||||
Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage()));
|
||||
Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage()));
|
||||
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage()));
|
||||
|
|
@ -262,6 +266,7 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||
processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation());
|
||||
processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight());
|
||||
processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled());
|
||||
processModel.setPaymentSentPayoutTxStale(proto.getPaymentSentPayoutTxStale());
|
||||
|
||||
// nullable
|
||||
processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature()));
|
||||
|
|
|
|||
|
|
@ -72,11 +72,12 @@ public class SellerProtocol extends DisputeProtocol {
|
|||
ThreadUtils.execute(() -> {
|
||||
if (!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return;
|
||||
synchronized (trade.getLock()) {
|
||||
if (!!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return;
|
||||
if (!((SellerTrade) trade).needsToResendPaymentReceivedMessages()) return;
|
||||
latchTrade();
|
||||
given(anyPhase(Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(SellerEvent.STARTUP))
|
||||
.setup(tasks(
|
||||
SellerPreparePaymentReceivedMessage.class,
|
||||
SellerSendPaymentReceivedMessageToBuyer.class,
|
||||
SellerSendPaymentReceivedMessageToArbitrator.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
|
|
@ -116,6 +117,16 @@ public class SellerProtocol extends DisputeProtocol {
|
|||
|
||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
|
||||
|
||||
// advance trade state
|
||||
if (trade.isPaymentSent() || trade.isPaymentReceived()) {
|
||||
trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT);
|
||||
} else {
|
||||
errorMessageHandler.handleErrorMessage("Cannot confirm payment received for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " in state " + trade.getState());
|
||||
return;
|
||||
}
|
||||
|
||||
// process on trade thread
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
|
|
@ -137,10 +148,9 @@ public class SellerProtocol extends DisputeProtocol {
|
|||
resultHandler.handleResult();
|
||||
}, (errorMessage) -> {
|
||||
log.warn("Error confirming payment received, reverting state to {}, error={}", Trade.State.BUYER_SENT_PAYMENT_SENT_MSG, errorMessage);
|
||||
trade.resetToPaymentSentState();
|
||||
if (!trade.isPayoutPublished()) trade.resetToPaymentSentState();
|
||||
handleTaskRunnerFault(event, errorMessage);
|
||||
})))
|
||||
.run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT))
|
||||
.executeTasks(true);
|
||||
} catch (Exception e) {
|
||||
errorMessageHandler.handleErrorMessage("Error confirming payment received: " + e.getMessage());
|
||||
|
|
|
|||
|
|
@ -256,7 +256,11 @@ public final class TradePeer implements PersistablePayload {
|
|||
}
|
||||
|
||||
public boolean isPaymentReceivedMessageReceived() {
|
||||
return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX || paymentReceivedMessageStateProperty.get() == MessageState.NACKED;
|
||||
return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX;
|
||||
}
|
||||
|
||||
public boolean isPaymentReceivedMessageArrived() {
|
||||
return paymentReceivedMessageStateProperty.get() == MessageState.ARRIVED;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ import haveno.common.crypto.PubKeyRing;
|
|||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
import haveno.common.proto.network.NetworkEnvelope;
|
||||
import haveno.common.taskrunner.Task;
|
||||
import haveno.core.network.MessageState;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.support.messages.ChatMessage;
|
||||
import haveno.core.trade.ArbitratorTrade;
|
||||
import haveno.core.trade.BuyerTrade;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
|
|
@ -100,7 +100,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
|
||||
public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 60 : 180;
|
||||
private static final String TIMEOUT_REACHED = "Timeout reached.";
|
||||
public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions
|
||||
public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other protocol functions
|
||||
public static final int REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS = 2; // request connection switch on even attempts
|
||||
public static final long REPROCESS_DELAY_MS = 5000;
|
||||
public static final String LOG_HIGHLIGHT = ""; // TODO: how to highlight some logs with cyan? ("\u001B[36m")? coloring works in the terminal but prints character literals to .log files
|
||||
|
|
@ -118,6 +118,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
private int reprocessPaymentSentMessageCount;
|
||||
private int reprocessPaymentReceivedMessageCount;
|
||||
private boolean makerInitTradeRequestHasBeenNacked = false;
|
||||
private PaymentReceivedMessage lastAckedPaymentReceivedMessage = null;
|
||||
|
||||
private static int MAX_PAYMENT_RECEIVED_NACKS = 5;
|
||||
private int numPaymentReceivedNacks = 0;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
|
|
@ -258,7 +262,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
||||
|
||||
// reprocess applicable messages
|
||||
trade.reprocessApplicableMessages();
|
||||
trade.initializeAfterMailboxMessages();
|
||||
}
|
||||
|
||||
// send deposits confirmed message if applicable
|
||||
|
|
@ -567,6 +571,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
removeMailboxMessageAfterProcessing(message);
|
||||
return;
|
||||
}
|
||||
if (message != trade.getBuyer().getPaymentSentMessage()) {
|
||||
log.warn("Ignoring PaymentSentMessage which was replaced by a newer message", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
latchTrade();
|
||||
expect(anyPhase()
|
||||
.with(message)
|
||||
|
|
@ -625,18 +633,31 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
|
||||
// save message for reprocessing
|
||||
trade.getSeller().setPaymentReceivedMessage(message);
|
||||
|
||||
// persist trade before processing on trade thread
|
||||
trade.persistNow(() -> {
|
||||
|
||||
// process message on trade thread
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) {
|
||||
log.warn("Received another PaymentReceivedMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) {
|
||||
log.warn("Skipping processing PaymentReceivedMessage because the trade is not initialized or it's shutting down for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) {
|
||||
log.warn("Received another PaymentReceivedMessage after payout is published {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
return;
|
||||
}
|
||||
if (message != trade.getSeller().getPaymentReceivedMessage()) {
|
||||
log.warn("Ignoring PaymentReceivedMessage which was replaced by a newer message for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
if (lastAckedPaymentReceivedMessage != null && lastAckedPaymentReceivedMessage.equals(trade.getSeller().getPaymentReceivedMessage())) {
|
||||
log.warn("Ignoring PaymentReceivedMessage which was already processed and responded to for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
|
|
@ -662,22 +683,32 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
ProcessPaymentReceivedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
lastAckedPaymentReceivedMessage = message;
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
log.warn("Error processing payment received message: " + errorMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// schedule to reprocess message unless deleted
|
||||
// schedule to reprocess message or nack
|
||||
if (trade.getSeller().getPaymentReceivedMessage() != null) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentReceivedMessageCount++;
|
||||
maybeReprocessPaymentReceivedMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount));
|
||||
if (reprocessOnError) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentReceivedMessageCount++;
|
||||
maybeReprocessPaymentReceivedMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount));
|
||||
}
|
||||
unlatchTrade();
|
||||
} else {
|
||||
handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // otherwise send nack
|
||||
|
||||
// export fresh multisig info for nack
|
||||
trade.exportMultisigHex();
|
||||
|
||||
// handle payout error
|
||||
lastAckedPaymentReceivedMessage = message;
|
||||
trade.onPayoutError(false, null);
|
||||
handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // send nack
|
||||
}
|
||||
unlatchTrade();
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
|
|
@ -743,7 +774,17 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
||||
boolean processOnTradeThread = !ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName()); // handle chat message acks off trade thread for responsiveness if the thread is busy
|
||||
if (processOnTradeThread) {
|
||||
ThreadUtils.execute(() -> onAckMessageAux(ackMessage, sender), trade.getId());
|
||||
} else {
|
||||
onAckMessageAux(ackMessage, sender);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this has grown in complexity over time and could use refactoring
|
||||
private void onAckMessageAux(AckMessage ackMessage, NodeAddress sender) {
|
||||
|
||||
// ignore if trade is completely finished
|
||||
if (trade.isFinished()) return;
|
||||
|
||||
|
|
@ -769,11 +810,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
// TODO: arbitrator may nack maker's InitTradeRequest if reserve tx has become invalid (e.g. check_tx_key shows 0 funds received). recreate reserve tx in this case
|
||||
if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) {
|
||||
if (ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED)) {
|
||||
// use default postprocessing to cancel maker's trade if arbitrator cannot send message to taker
|
||||
} else {
|
||||
// use default postprocessing
|
||||
if (makerInitTradeRequestHasBeenNacked) {
|
||||
handleSecondMakerInitTradeRequestNack(ackMessage);
|
||||
// use default postprocessing to cancel maker's trade
|
||||
// use default postprocessing
|
||||
} else {
|
||||
makerInitTradeRequestHasBeenNacked = true;
|
||||
handleFirstMakerInitTradeRequestNack(ackMessage);
|
||||
|
|
@ -798,6 +838,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
|
||||
// handle ack message for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time
|
||||
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
|
||||
if (!trade.isPaymentMarkedSent()) {
|
||||
log.warn("Received AckMessage for PaymentSentMessage but trade is in unexpected state, ignoring. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid());
|
||||
return;
|
||||
}
|
||||
if (peer == trade.getSeller()) {
|
||||
trade.getSeller().setPaymentSentAckMessage(ackMessage);
|
||||
if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||
|
|
@ -807,63 +851,67 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
trade.getArbitrator().setPaymentSentAckMessage(ackMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
} else {
|
||||
log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage());
|
||||
log.warn("Received AckMessage from unexpected peer. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// handle ack message for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time
|
||||
// TODO: trade state can be reset twice if both peers nack before published payout is detected
|
||||
// TODO: do not reset state if payment received message is acknowledged because payout is likely broadcast?
|
||||
if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) {
|
||||
|
||||
// ack message from buyer
|
||||
if (peer == trade.getBuyer()) {
|
||||
trade.getBuyer().setPaymentReceivedAckMessage(ackMessage);
|
||||
processModel.getTradeManager().persistNow(null);
|
||||
|
||||
// handle successful ack
|
||||
if (ackMessage.isSuccess()) {
|
||||
|
||||
// validate state
|
||||
if (!trade.isPaymentMarkedReceived()) {
|
||||
log.warn("Received AckMessage for PaymentReceivedMessage but trade is in unexpected state, ignoring. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid());
|
||||
return;
|
||||
}
|
||||
|
||||
trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG);
|
||||
processModel.getTradeManager().persistNow(null);
|
||||
}
|
||||
|
||||
// handle nack
|
||||
else {
|
||||
log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
|
||||
log.warn("We received a NACK for our PaymentReceivedMessage to the buyer for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getErrorMessage());
|
||||
|
||||
// nack includes updated multisig hex since v1.1.1
|
||||
if (ackMessage.getUpdatedMultisigHex() != null) {
|
||||
trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
|
||||
|
||||
// reset state if not processed
|
||||
if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) {
|
||||
log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
trade.resetToPaymentSentState();
|
||||
}
|
||||
processModel.getTradeManager().persistNow(null);
|
||||
boolean autoResent = onPayoutError(true, peer);
|
||||
if (autoResent) return; // skip remaining processing if auto resent
|
||||
}
|
||||
}
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
// ack message from arbitrator
|
||||
else if (peer == trade.getArbitrator()) {
|
||||
trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage);
|
||||
processModel.getTradeManager().persistNow(null);
|
||||
|
||||
// handle nack
|
||||
if (!ackMessage.isSuccess()) {
|
||||
log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
log.warn("We received a NACK for our PaymentReceivedMessage to the arbitrator for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getErrorMessage());
|
||||
|
||||
// nack includes updated multisig hex since v1.1.1
|
||||
if (ackMessage.getUpdatedMultisigHex() != null) {
|
||||
trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
|
||||
|
||||
// reset state if not processed
|
||||
if (trade.isPaymentReceived() && !trade.isPayoutPublished() && !isPaymentReceivedMessageAckedByEither()) {
|
||||
log.warn("Resetting state to payment sent for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
trade.resetToPaymentSentState();
|
||||
}
|
||||
processModel.getTradeManager().persistNow(null);
|
||||
boolean autoResent = onPayoutError(true, peer);
|
||||
if (autoResent) return; // skip remaining processing if auto resent
|
||||
}
|
||||
}
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
} else {
|
||||
log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage());
|
||||
log.warn("Received AckMessage from unexpected peer. Sender={}, trade={} {}, state={}, success={}, error={}, messageUid={}", sender, trade.getClass().getSimpleName(), trade.getId(), trade.getState(), ackMessage.isSuccess(), ackMessage.getErrorMessage(), ackMessage.getSourceUid());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -886,6 +934,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
trade.onAckMessage(ackMessage, sender);
|
||||
}
|
||||
|
||||
private boolean onPayoutError(boolean syncAndPoll, TradePeer peer) {
|
||||
|
||||
// prevent infinite nack loop with max attempts
|
||||
numPaymentReceivedNacks++;
|
||||
if (numPaymentReceivedNacks > MAX_PAYMENT_RECEIVED_NACKS) {
|
||||
log.warn("Maximum number of PaymentReceivedMessage NACKs reached for {} {}, not retrying", trade.getClass().getSimpleName(), trade.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// handle payout error
|
||||
return trade.onPayoutError(syncAndPoll, peer);
|
||||
}
|
||||
|
||||
private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) {
|
||||
log.warn("Maker received NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage());
|
||||
ThreadUtils.execute(() -> {
|
||||
|
|
@ -928,12 +989,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
log.warn(warningMessage);
|
||||
}
|
||||
|
||||
private boolean isPaymentReceivedMessageAckedByEither() {
|
||||
if (trade.getBuyer().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true;
|
||||
if (trade.getArbitrator().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) {
|
||||
sendAckMessage(peer, message, result, errorMessage, null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,9 +113,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
|||
boolean isFromBuyer = sender == trade.getBuyer();
|
||||
BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee();
|
||||
BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount();
|
||||
BigInteger securityDeposit = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
BigInteger securityDepositBeforeMiningFee = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
String depositAddress = processModel.getMultisigAddress();
|
||||
sender.setSecurityDeposit(securityDeposit);
|
||||
sender.setSecurityDeposit(securityDepositBeforeMiningFee);
|
||||
|
||||
// verify deposit tx
|
||||
boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit();
|
||||
|
|
@ -126,7 +126,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
|||
tradeFee,
|
||||
trade.getProcessModel().getTradeFeeAddress(),
|
||||
sendTradeAmount,
|
||||
securityDeposit,
|
||||
securityDepositBeforeMiningFee,
|
||||
depositAddress,
|
||||
sender.getDepositTxHash(),
|
||||
request.getDepositTxHex(),
|
||||
|
|
@ -141,7 +141,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
|||
}
|
||||
|
||||
// update trade state
|
||||
sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||
sender.setSecurityDeposit(securityDepositBeforeMiningFee.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||
sender.setDepositTxFee(verifiedTx.getFee());
|
||||
sender.setDepositTxHex(request.getDepositTxHex());
|
||||
sender.setDepositTxKey(request.getDepositTxKey());
|
||||
|
|
@ -183,7 +183,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
|||
try {
|
||||
monerod.relayTxsByHash(txHashes); // call will error if txs are already confirmed, but they're still relayed
|
||||
} catch (Exception e) {
|
||||
log.warn("Error relaying deposit txs: " + e.getMessage());
|
||||
log.warn("Error relaying deposit txs for trade {}. They could already be confirmed. Error={}", trade.getId(), e.getMessage());
|
||||
}
|
||||
depositTxsRelayed = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public class ProcessDepositResponse extends TradeTask {
|
|||
// handle error
|
||||
DepositResponse message = (DepositResponse) processModel.getTradeMessage();
|
||||
if (message.getErrorMessage() != null) {
|
||||
log.warn("Deposit response for {} {} has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage());
|
||||
log.warn("Deposit response has error message for {} {}: {}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage());
|
||||
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
|
||||
trade.setInitError(new RuntimeException(message.getErrorMessage()));
|
||||
complete();
|
||||
|
|
@ -52,7 +52,7 @@ public class ProcessDepositResponse extends TradeTask {
|
|||
try {
|
||||
model.getXmrWalletService().getMonerod().submitTxHex(trade.getSelf().getDepositTxHex());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to redundantly publish deposit transaction for {} {}", trade.getClass().getSimpleName(), trade.getShortId(), e);
|
||||
log.warn("Failed to redundantly publish deposit transaction for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
|
||||
}
|
||||
|
||||
// record security deposits
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||
|
||||
@Slf4j
|
||||
public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
|
||||
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
|
@ -122,9 +123,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||
complete();
|
||||
} catch (Throwable t) {
|
||||
|
||||
// do not reprocess illegal argument
|
||||
// handle illegal exception
|
||||
if (HavenoUtils.isIllegal(t)) {
|
||||
trade.getSeller().setPaymentReceivedMessage(null); // do not reprocess
|
||||
trade.getSeller().setPaymentReceivedMessage(null); // stops reprocessing
|
||||
trade.requestPersistence();
|
||||
}
|
||||
|
||||
|
|
@ -155,8 +156,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||
// verify and publish payout tx
|
||||
if (!trade.isPayoutPublished()) {
|
||||
try {
|
||||
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
||||
if (isSigned) {
|
||||
if (message.getPayoutTxId() != null && trade.isBuyer()) {
|
||||
trade.processBuyerPayout(message.getPayoutTxId()); // buyer can validate payout tx by id with main wallet (in case of multisig issues)
|
||||
} else if (message.getSignedPayoutTxHex() != null) {
|
||||
log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId());
|
||||
trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -40,48 +40,61 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
|||
// check connection
|
||||
trade.verifyDaemonConnection();
|
||||
|
||||
// handle first time preparation
|
||||
if (trade.getArbitrator().getPaymentReceivedMessage() == null) {
|
||||
|
||||
// synchronize on lock for wallet operations
|
||||
// import and export multisig hex if payout already published
|
||||
if (trade.isPayoutPublished()) {
|
||||
synchronized (trade.getWalletLock()) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
|
||||
// import multisig hex unless already signed
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
if (trade.walletExists()) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
trade.importMultisigHex();
|
||||
}
|
||||
|
||||
// verify, sign, and publish payout tx if given
|
||||
if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null) {
|
||||
try {
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
|
||||
trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true);
|
||||
} else {
|
||||
log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId());
|
||||
trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}. Creating new unsigned payout tx. error={}. ", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e);
|
||||
createUnsignedPayoutTx();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise create unsigned payout tx
|
||||
else if (trade.getSelf().getUnsignedPayoutTxHex() == null) {
|
||||
createUnsignedPayoutTx();
|
||||
trade.exportMultisigHex();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
|
||||
} else {
|
||||
|
||||
// republish payout tx from previous message
|
||||
log.info("Seller re-verifying and publishing signed payout tx for trade {}", trade.getId());
|
||||
trade.processPayoutTx(trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true);
|
||||
// process or create payout tx
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
|
||||
// synchronize on lock for wallet operations
|
||||
synchronized (trade.getWalletLock()) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
|
||||
// import multisig hex unless already signed
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
trade.importMultisigHex();
|
||||
}
|
||||
|
||||
// verify, sign, and publish payout tx if given
|
||||
if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null && !trade.getProcessModel().isPaymentSentPayoutTxStale()) {
|
||||
try {
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
|
||||
trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true);
|
||||
} else {
|
||||
log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId());
|
||||
trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}. Creating new unsigned payout tx. error={}. ", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e);
|
||||
createUnsignedPayoutTx();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise create unsigned payout tx
|
||||
else if (trade.getSelf().getUnsignedPayoutTxHex() == null) {
|
||||
createUnsignedPayoutTx();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// republish payout tx from previous message
|
||||
log.info("Seller re-verifying and publishing signed payout tx for trade {}", trade.getId());
|
||||
trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
|
||||
}
|
||||
}
|
||||
|
||||
// close open disputes
|
||||
|
|
@ -99,6 +112,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
|||
|
||||
private void createUnsignedPayoutTx() {
|
||||
log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
|
||||
trade.getProcessModel().setPaymentSentPayoutTxStale(true);
|
||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||
trade.updatePayout(payoutTx);
|
||||
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@Slf4j
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||
|
|
@ -69,6 +71,9 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
|||
private static final int MAX_RESEND_ATTEMPTS = 20;
|
||||
private int delayInMin = 10;
|
||||
private int resendCounter = 0;
|
||||
private String unsignedPayoutTxHex = null;
|
||||
private String signedPayoutTxHex = null;
|
||||
private String updatedMultisigHex = null;
|
||||
|
||||
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
|
|
@ -123,20 +128,30 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
|||
// messages where only the one which gets processed by the peer would be removed we use the same uid. All
|
||||
// other data stays the same when we re-send the message at any time later.
|
||||
String deterministicId = HavenoUtils.getDeterministicId(trade, PaymentReceivedMessage.class, getReceiverNodeAddress());
|
||||
boolean deferPublishPayout = trade.isPayoutPublished() || trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(); // informs receiver to expect payout so delay processing
|
||||
boolean deferPublishPayout = getReceiver() == trade.getArbitrator() && (trade.isPayoutPublished() || trade.getOtherPeer(getReceiver()).isPaymentReceivedMessageArrived()); // informs receiver to expect payout so delay processing
|
||||
unsignedPayoutTxHex = trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null; // signed
|
||||
signedPayoutTxHex = trade.getPayoutTxHex();
|
||||
updatedMultisigHex = trade.getSelf().getUpdatedMultisigHex();
|
||||
PaymentReceivedMessage message = new PaymentReceivedMessage(
|
||||
tradeId,
|
||||
processModel.getMyNodeAddress(),
|
||||
deterministicId,
|
||||
trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null, // unsigned // TODO: phase in after next update to clear old style trades
|
||||
trade.getPayoutTxHex() == null ? null : trade.getPayoutTxHex(), // signed
|
||||
trade.getSelf().getUpdatedMultisigHex(),
|
||||
unsignedPayoutTxHex,
|
||||
signedPayoutTxHex,
|
||||
updatedMultisigHex,
|
||||
deferPublishPayout,
|
||||
trade.getTradePeer().getAccountAgeWitness(),
|
||||
signedWitness,
|
||||
getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null // buyer already has payment sent message
|
||||
getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null, // buyer already has payment sent message,
|
||||
trade.getPayoutTxId()
|
||||
);
|
||||
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex");
|
||||
|
||||
// verify message
|
||||
if (trade.isPayoutPublished()) {
|
||||
checkArgument(message.getUpdatedMultisigHex() != null || message.getPayoutTxId() != null, "PaymentReceivedMessage does not include updated multisig hex or payout tx id after payout published");
|
||||
} else {
|
||||
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex");
|
||||
}
|
||||
|
||||
// sign message
|
||||
try {
|
||||
|
|
@ -240,6 +255,9 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
|||
if (isMessageReceived()) return true; // stop if message received
|
||||
if (!trade.isPaymentReceived()) return true; // stop if trade state reset
|
||||
if (trade.isPayoutPublished() && !((SellerTrade) trade).resendPaymentReceivedMessagesWithinDuration()) return true; // stop if payout is published and we are not in the resend period
|
||||
if (unsignedPayoutTxHex != null && !StringUtils.equals(unsignedPayoutTxHex, trade.getSelf().getUnsignedPayoutTxHex())) return true;
|
||||
if (signedPayoutTxHex != null && !StringUtils.equals(signedPayoutTxHex, trade.getPayoutTxHex())) return true;
|
||||
if (updatedMultisigHex != null && !StringUtils.equals(updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex())) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ 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));
|
||||
UserThread.execute(() -> {
|
||||
UserThread.await(() -> this.percentage.set(percentage)); // TODO: these awaits are jenky
|
||||
});
|
||||
}
|
||||
|
||||
public void doneDownload() {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import java.util.Optional;
|
|||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
|
|
@ -28,9 +26,10 @@ import monero.wallet.model.MoneroWalletListener;
|
|||
public abstract class XmrWalletBase {
|
||||
|
||||
// constants
|
||||
public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120;
|
||||
public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 180;
|
||||
public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100;
|
||||
public static final int SAVE_WALLET_DELAY_SECONDS = 300;
|
||||
private static final String SYNC_PROGRESS_TIMEOUT_MSG = "Sync progress timeout called";
|
||||
|
||||
// inherited
|
||||
protected MoneroWallet wallet;
|
||||
|
|
@ -70,74 +69,96 @@ public abstract class XmrWalletBase {
|
|||
|
||||
public void syncWithProgress(boolean repeatSyncToLatestHeight) {
|
||||
synchronized (walletLock) {
|
||||
try {
|
||||
|
||||
// set initial state
|
||||
isSyncingWithProgress = true;
|
||||
syncProgressError = null;
|
||||
long targetHeightAtStart = xmrConnectionService.getTargetHeight();
|
||||
syncStartHeight = walletHeight.get();
|
||||
updateSyncProgress(syncStartHeight, targetHeightAtStart);
|
||||
// set initial state
|
||||
if (isSyncingWithProgress) log.warn("Syncing with progress while already syncing with progress. That should never happen");
|
||||
resetSyncProgressTimeout();
|
||||
isSyncingWithProgress = true;
|
||||
syncProgressError = null;
|
||||
long targetHeightAtStart = xmrConnectionService.getTargetHeight();
|
||||
syncStartHeight = walletHeight.get();
|
||||
updateSyncProgress(syncStartHeight, targetHeightAtStart);
|
||||
|
||||
// test connection changing on startup before wallet synced
|
||||
if (testReconnectOnStartup) {
|
||||
UserThread.runAfter(() -> {
|
||||
log.warn("Testing connection change on startup before wallet synced");
|
||||
if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2);
|
||||
else xmrConnectionService.setConnection(testReconnectMonerod1);
|
||||
}, 1);
|
||||
testReconnectOnStartup = false; // only run once
|
||||
}
|
||||
// test connection changing on startup before wallet synced
|
||||
if (testReconnectOnStartup) {
|
||||
UserThread.runAfter(() -> {
|
||||
log.warn("Testing connection change on startup before wallet synced");
|
||||
if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2);
|
||||
else xmrConnectionService.setConnection(testReconnectMonerod1);
|
||||
}, 1);
|
||||
testReconnectOnStartup = false; // only run once
|
||||
}
|
||||
|
||||
// native wallet provides sync notifications
|
||||
if (wallet instanceof MoneroWalletFull) {
|
||||
if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
|
||||
wallet.sync(new MoneroWalletListener() {
|
||||
@Override
|
||||
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
|
||||
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
|
||||
updateSyncProgress(height, appliedTargetHeight);
|
||||
}
|
||||
});
|
||||
setWalletSyncedWithProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
// start polling wallet for progress
|
||||
syncProgressLatch = new CountDownLatch(1);
|
||||
syncProgressLooper = new TaskLooper(() -> {
|
||||
long height;
|
||||
try {
|
||||
height = wallet.getHeight(); // can get read timeout while syncing
|
||||
} catch (Exception e) {
|
||||
if (wallet != null && !isShutDownStarted) {
|
||||
log.warn("Error getting wallet height while syncing with progress: " + e.getMessage());
|
||||
log.warn(ExceptionUtils.getStackTrace(e));
|
||||
}
|
||||
|
||||
// stop polling and release latch
|
||||
syncProgressError = e;
|
||||
syncProgressLatch.countDown();
|
||||
// native wallet provides sync notifications
|
||||
if (wallet instanceof MoneroWalletFull) {
|
||||
if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
|
||||
wallet.sync(new MoneroWalletListener() {
|
||||
@Override
|
||||
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
|
||||
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
|
||||
updateSyncProgress(height, appliedTargetHeight);
|
||||
}
|
||||
});
|
||||
setWalletSyncedWithProgress();
|
||||
return;
|
||||
}
|
||||
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
|
||||
updateSyncProgress(height, appliedTargetHeight);
|
||||
if (height >= appliedTargetHeight) {
|
||||
setWalletSyncedWithProgress();
|
||||
syncProgressLatch.countDown();
|
||||
|
||||
// start polling wallet for progress
|
||||
syncProgressLatch = new CountDownLatch(1);
|
||||
syncProgressLooper = new TaskLooper(() -> {
|
||||
|
||||
// stop if shutdown or null wallet
|
||||
if (isShutDownStarted || wallet == null) {
|
||||
syncProgressError = new RuntimeException("Shut down or wallet has become null while syncing with progress");
|
||||
syncProgressLatch.countDown();
|
||||
return;
|
||||
}
|
||||
|
||||
// get height
|
||||
long height;
|
||||
try {
|
||||
height = wallet.getHeight(); // can get read timeout while syncing
|
||||
} catch (Exception e) {
|
||||
if (wallet != null && !isShutDownStarted) {
|
||||
log.warn("Error getting wallet height while syncing with progress: " + e.getMessage());
|
||||
}
|
||||
if (wallet == null) {
|
||||
syncProgressError = new RuntimeException("Wallet has become null while syncing with progress");
|
||||
syncProgressLatch.countDown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// update sync progress
|
||||
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
|
||||
updateSyncProgress(height, appliedTargetHeight);
|
||||
if (height >= appliedTargetHeight) {
|
||||
setWalletSyncedWithProgress();
|
||||
syncProgressLatch.countDown();
|
||||
}
|
||||
});
|
||||
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
|
||||
syncProgressLooper.start(1000);
|
||||
|
||||
// wait for sync to complete
|
||||
HavenoUtils.awaitLatch(syncProgressLatch);
|
||||
|
||||
// stop polling
|
||||
syncProgressLooper.stop();
|
||||
syncProgressTimeout.stop();
|
||||
if (wallet != null) { // can become null if interrupted by force close
|
||||
if (syncProgressError == null || !HavenoUtils.isUnresponsive(syncProgressError)) { // TODO: skipping stop sync if unresponsive because wallet will hang. if unresponsive, wallet is assumed to be force restarted by caller, but that should be done internally here instead of externally?
|
||||
wallet.stopSyncing();
|
||||
}
|
||||
}
|
||||
});
|
||||
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
|
||||
syncProgressLooper.start(1000);
|
||||
|
||||
// wait for sync to complete
|
||||
HavenoUtils.awaitLatch(syncProgressLatch);
|
||||
|
||||
// stop polling
|
||||
syncProgressLooper.stop();
|
||||
syncProgressTimeout.stop();
|
||||
if (wallet != null) wallet.stopSyncing(); // can become null if interrupted by force close
|
||||
isSyncingWithProgress = false;
|
||||
if (syncProgressError != null) throw new RuntimeException(syncProgressError);
|
||||
saveWallet();
|
||||
if (syncProgressError != null) throw new RuntimeException(syncProgressError);
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
} finally {
|
||||
isSyncingWithProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +182,10 @@ public abstract class XmrWalletBase {
|
|||
|
||||
// --------------------------------- ABSTRACT -----------------------------
|
||||
|
||||
public static boolean isSyncWithProgressTimeout(Throwable e) {
|
||||
return e.getMessage().contains(SYNC_PROGRESS_TIMEOUT_MSG);
|
||||
}
|
||||
|
||||
public abstract void saveWallet();
|
||||
|
||||
public abstract void requestSaveWallet();
|
||||
|
|
@ -170,31 +195,33 @@ public abstract class XmrWalletBase {
|
|||
// ------------------------------ PRIVATE HELPERS -------------------------
|
||||
|
||||
private void updateSyncProgress(long height, long targetHeight) {
|
||||
resetSyncProgressTimeout();
|
||||
UserThread.execute(() -> {
|
||||
|
||||
// set wallet height
|
||||
walletHeight.set(height);
|
||||
// reset progress timeout if height advanced
|
||||
if (height != walletHeight.get()) {
|
||||
resetSyncProgressTimeout();
|
||||
}
|
||||
|
||||
// new wallet reports height 1 before synced
|
||||
if (height == 1) {
|
||||
downloadListener.progress(0, targetHeight - height, null);
|
||||
return;
|
||||
}
|
||||
// set wallet height
|
||||
walletHeight.set(height);
|
||||
|
||||
// set progress
|
||||
long blocksLeft = targetHeight - walletHeight.get();
|
||||
if (syncStartHeight == null) syncStartHeight = walletHeight.get();
|
||||
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight));
|
||||
downloadListener.progress(percent, blocksLeft, null);
|
||||
});
|
||||
// new wallet reports height 1 before synced
|
||||
if (height == 1) {
|
||||
downloadListener.progress(0, targetHeight - height, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// set progress
|
||||
long blocksLeft = targetHeight - height;
|
||||
if (syncStartHeight == null) syncStartHeight = height;
|
||||
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) height - syncStartHeight) / (double) (targetHeight - syncStartHeight));
|
||||
downloadListener.progress(percent, blocksLeft, null);
|
||||
}
|
||||
|
||||
private synchronized void resetSyncProgressTimeout() {
|
||||
if (syncProgressTimeout != null) syncProgressTimeout.stop();
|
||||
syncProgressTimeout = UserThread.runAfter(() -> {
|
||||
if (isShutDownStarted) return;
|
||||
syncProgressError = new RuntimeException("Sync progress timeout called");
|
||||
syncProgressError = new RuntimeException(SYNC_PROGRESS_TIMEOUT_MSG);
|
||||
syncProgressLatch.countDown();
|
||||
}, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
|
@ -202,6 +229,6 @@ public abstract class XmrWalletBase {
|
|||
private void setWalletSyncedWithProgress() {
|
||||
wasWalletSynced = true;
|
||||
isSyncingWithProgress = false;
|
||||
syncProgressTimeout.stop();
|
||||
if (syncProgressTimeout != null) syncProgressTimeout.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1077,7 +1077,7 @@ public class XmrWalletService extends XmrWalletBase {
|
|||
// swap trade payout to available if applicable
|
||||
if (tradeManager == null) return;
|
||||
Trade trade = tradeManager.getTrade(offerId);
|
||||
if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||
if (trade == null || trade.isPayoutFinalized()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||
}
|
||||
|
||||
public synchronized void swapPayoutAddressEntryToAvailable(String offerId) {
|
||||
|
|
@ -1221,7 +1221,7 @@ public class XmrWalletService extends XmrWalletBase {
|
|||
Stream<XmrAddressEntry> available = getFundedAvailableAddressEntries().stream();
|
||||
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream());
|
||||
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent()));
|
||||
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked()));
|
||||
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutFinalized()));
|
||||
return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0);
|
||||
}
|
||||
|
||||
|
|
@ -2020,7 +2020,7 @@ public class XmrWalletService extends XmrWalletBase {
|
|||
doPollWallet(true);
|
||||
}
|
||||
|
||||
private void doPollWallet(boolean updateTxs) {
|
||||
public void doPollWallet(boolean updateTxs) {
|
||||
|
||||
// skip if shut down started
|
||||
if (isShutDownStarted) return;
|
||||
|
|
@ -2073,6 +2073,7 @@ public class XmrWalletService extends XmrWalletBase {
|
|||
synchronized (walletLock) { // avoid long fetch from blocking other operations
|
||||
synchronized (HavenoUtils.getDaemonLock()) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
if (lastPollTxsTimestamp == 0) lastPollTxsTimestamp = System.currentTimeMillis(); // set initial timestamp
|
||||
try {
|
||||
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
lastPollTxsTimestamp = System.currentTimeMillis();
|
||||
|
|
|
|||
|
|
@ -660,6 +660,7 @@ portfolio.pending.unconfirmedTooLong=Deposit transactions on trade {0} are still
|
|||
If the problem persists, contact Haveno support [HYPERLINK:https://matrix.to/#/#haveno:monero.social].
|
||||
|
||||
portfolio.pending.step1.waitForConf=Wait for blockchain confirmations
|
||||
portfolio.pending.step2_buyer.additionalConf=Deposits have reached 10 confirmations.\nFor extra security, we recommend waiting {0} confirmations before sending payment.\nProceed early at your own risk.
|
||||
portfolio.pending.step2_buyer.startPayment=Start payment
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Wait until payment has been sent
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Wait until payment arrived
|
||||
|
|
@ -936,6 +937,8 @@ portfolio.pending.support.headline.getHelp=Need help?
|
|||
portfolio.pending.support.button.getHelp=Open Trader Chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Check payment
|
||||
portfolio.pending.support.headline.periodOver=Trade period is over
|
||||
portfolio.pending.support.headline.depositTxMissing=Missing deposit transaction
|
||||
portfolio.pending.support.depositTxMissing=A deposit transaction is missing for this trade. Open a support ticket to contact an arbitrator for assistance.
|
||||
|
||||
portfolio.pending.arbitrationRequested=Arbitration requested
|
||||
portfolio.pending.mediationRequested=Mediation requested
|
||||
|
|
@ -2338,6 +2341,7 @@ notification.ticket.headline=Support ticket for trade with ID {0}
|
|||
notification.trade.completed=The trade is now completed, and you can withdraw your funds.
|
||||
notification.trade.accepted=Your offer has been accepted by a XMR {0}.
|
||||
notification.trade.unlocked=Your trade has been confirmed.\nYou can start the payment now.
|
||||
notification.trade.finalized=The trade has {0} confirmations.\nYou can start the payment now.
|
||||
notification.trade.paymentSent=The XMR buyer has sent the payment.
|
||||
notification.trade.selectTrade=Select trade
|
||||
notification.trade.peerOpenedDispute=Your trading peer has opened a {0}.
|
||||
|
|
|
|||
|
|
@ -625,6 +625,7 @@ portfolio.pending.unconfirmedTooLong=Vkladové transakce obchodu {0} jsou stále
|
|||
Pokud problém přetrvává, kontaktujte podporu Haveno [HYPERLINK:https://matrix.to/#/#haveno:monero.social].
|
||||
|
||||
portfolio.pending.step1.waitForConf=Počkejte na potvrzení na blockchainu
|
||||
portfolio.pending.step2_buyer.additionalConf=Vklady dosáhly 10 potvrzení.\nPro vyšší bezpečnost doporučujeme počkat na {0} potvrzení před odesláním platby.\nPokračujte dříve na vlastní riziko.
|
||||
portfolio.pending.step2_buyer.startPayment=Zahajte platbu
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Počkejte, než začne platba
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Počkejte, než dorazí platba
|
||||
|
|
@ -901,6 +902,8 @@ portfolio.pending.support.headline.getHelp=Potřebujete pomoc?
|
|||
portfolio.pending.support.button.getHelp=Otevřít obchodní chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Zkontrolujte platbu
|
||||
portfolio.pending.support.headline.periodOver=Obchodní období skončilo
|
||||
portfolio.pending.support.headline.depositTxMissing=Chybějící vkladová transakce
|
||||
portfolio.pending.support.depositTxMissing=U tohoto obchodu chybí transakce vkladu. Otevřete podporu, abyste kontaktovali rozhodce a získali pomoc.
|
||||
|
||||
portfolio.pending.arbitrationRequested=Požádáno o arbitráž
|
||||
portfolio.pending.mediationRequested=Požádáno o mediaci
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ portfolio.closedTrades.deviation.help=Prozentuale Preisabweichung vom Markt
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Auf Blockchain-Bestätigung warten
|
||||
portfolio.pending.step2_buyer.additionalConf=Einzahlungen haben 10 Bestätigungen erreicht.\nFür zusätzliche Sicherheit empfehlen wir, {0} Bestätigungen abzuwarten, bevor Sie die Zahlung senden.\nEin früheres Vorgehen erfolgt auf eigenes Risiko.
|
||||
portfolio.pending.step2_buyer.startPayment=Zahlung beginnen
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Auf Zahlungsbeginn warten
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Auf Zahlungseingang warten
|
||||
|
|
@ -792,6 +793,8 @@ portfolio.pending.support.text.getHelp=Wenn Sie irgendwelche Probleme haben, kö
|
|||
portfolio.pending.support.button.getHelp=Trader Chat öffnen
|
||||
portfolio.pending.support.headline.halfPeriodOver=Zahlung überprüfen
|
||||
portfolio.pending.support.headline.periodOver=Die Handelsdauer ist abgelaufen
|
||||
portfolio.pending.support.headline.depositTxMissing=Fehlende Einzahlungstransaktion
|
||||
portfolio.pending.support.depositTxMissing=Für diesen Handel fehlt eine Einzahlungstransaktion. Öffnen Sie ein Support-Ticket, um einen Schlichter um Hilfe zu bitten.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediation beantragt
|
||||
portfolio.pending.refundRequested=Rückerstattung beantragt
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ portfolio.closedTrades.deviation.help=Desviación porcentual de precio de mercad
|
|||
portfolio.pending.invalidTx=Hay un problema con una transacción inválida o no encontrada.\n\nPor faovr NO envíe el pago de traditional o cryptos.\n\nAbra un ticket de soporte para obtener asistencia de un mediador.\n\nMensaje de error: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Esperar a la confirmación en la cadena de bloques
|
||||
portfolio.pending.step2_buyer.additionalConf=Los depósitos han alcanzado 10 confirmaciones.\nPara mayor seguridad, recomendamos esperar {0} confirmaciones antes de enviar el pago.\nProceda antes bajo su propio riesgo.
|
||||
portfolio.pending.step2_buyer.startPayment=Comenzar pago
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Esperar hasta que el pago se haya iniciado
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Esperar hasta que el pago haya llegado
|
||||
|
|
@ -792,6 +793,8 @@ portfolio.pending.support.text.getHelp=Si tiene algún problema puede intentar c
|
|||
portfolio.pending.support.button.getHelp=Abrir chat de intercambio
|
||||
portfolio.pending.support.headline.halfPeriodOver=Comprobar pago
|
||||
portfolio.pending.support.headline.periodOver=El periodo de intercambio se acabó
|
||||
portfolio.pending.support.headline.depositTxMissing=Transacción de depósito faltante
|
||||
portfolio.pending.support.depositTxMissing=Falta una transacción de depósito para este comercio. Abra un ticket de soporte para contactar a un árbitro y recibir asistencia.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediación solicitada
|
||||
portfolio.pending.refundRequested=Devolución de fondos solicitada
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=برای تأییدیه بلاک چین منتظر باشید
|
||||
portfolio.pending.step2_buyer.additionalConf=واریزها به ۱۰ تأیید رسیدهاند.\nبرای امنیت بیشتر، توصیه میکنیم قبل از ارسال پرداخت، {0} تأیید صبر کنید.\nاقدام زودهنگام با مسئولیت خودتان است.
|
||||
portfolio.pending.step2_buyer.startPayment=آغاز پرداخت
|
||||
portfolio.pending.step2_seller.waitPaymentSent=صبر کنید تا پرداخت شروع شود
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=صبر کنید تا پرداخت حاصل شود
|
||||
|
|
@ -791,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c
|
|||
portfolio.pending.support.button.getHelp=Open Trader Chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Check payment
|
||||
portfolio.pending.support.headline.periodOver=Trade period is over
|
||||
portfolio.pending.support.headline.depositTxMissing=تراکنش واریز مفقود شده
|
||||
portfolio.pending.support.depositTxMissing=برای این معامله، تراکنش واریز وجود ندارد. برای دریافت کمک با داور، یک تیکت پشتیبانی باز کنید.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediation requested
|
||||
portfolio.pending.refundRequested=Refund requested
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ portfolio.closedTrades.deviation.help=Pourcentage de déviation du prix par rapp
|
|||
portfolio.pending.invalidTx=Il y'a un problème avec une transaction manquante ou invalide.\n\nVeuillez NE PAS envoyer le payement Traditional ou crypto.\n\nOuvrez un ticket de support pour avoir l'aide d'un médiateur.\n\nMessage d'erreur: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Attendre la confirmation de la blockchain
|
||||
portfolio.pending.step2_buyer.additionalConf=Les dépôts ont atteint 10 confirmations.\nPour plus de sécurité, nous recommandons d’attendre {0} confirmations avant d’envoyer le paiement.\nProcédez plus tôt à vos propres risques.
|
||||
portfolio.pending.step2_buyer.startPayment=Initier le paiement
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Patientez jusqu'à ce que le paiement soit commencé.
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Patientez jusqu'à la réception du paiement
|
||||
|
|
@ -793,6 +794,8 @@ portfolio.pending.support.text.getHelp=Si vous rencontrez des problèmes, vous p
|
|||
portfolio.pending.support.button.getHelp=Ouvrir le chat de trade
|
||||
portfolio.pending.support.headline.halfPeriodOver=Vérifier le paiement
|
||||
portfolio.pending.support.headline.periodOver=Le délai alloué pour ce trade est écoulé.
|
||||
portfolio.pending.support.headline.depositTxMissing=Transaction de dépôt manquante
|
||||
portfolio.pending.support.depositTxMissing=Une transaction de dépôt est manquante pour cette opération. Ouvrez un ticket de support pour contacter un arbitre et obtenir de l’aide.
|
||||
|
||||
portfolio.pending.mediationRequested=Médiation demandée
|
||||
portfolio.pending.refundRequested=Remboursement demandé
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Attendi la conferma della blockchain
|
||||
portfolio.pending.step2_buyer.additionalConf=I depositi hanno raggiunto 10 conferme.\nPer maggiore sicurezza, consigliamo di attendere {0} conferme prima di inviare il pagamento.\nProcedi in anticipo a tuo rischio.
|
||||
portfolio.pending.step2_buyer.startPayment=Inizia il pagamento
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Attendi fino all'avvio del pagamento
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Attendi fino all'arrivo del pagamento
|
||||
|
|
@ -791,6 +792,8 @@ portfolio.pending.support.text.getHelp=In caso di problemi, puoi provare a conta
|
|||
portfolio.pending.support.button.getHelp=Apri la chat dello scambio
|
||||
portfolio.pending.support.headline.halfPeriodOver=Controlla il pagamento
|
||||
portfolio.pending.support.headline.periodOver=Il periodo di scambio è finito
|
||||
portfolio.pending.support.headline.depositTxMissing=Transazione di deposito mancante
|
||||
portfolio.pending.support.depositTxMissing=Manca una transazione di deposito per questa operazione. Apri un ticket di supporto per contattare un arbitro per assistenza.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediazione richiesta
|
||||
portfolio.pending.refundRequested=Rimborso richiesto
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ portfolio.closedTrades.deviation.help=市場からの割合価格偏差
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=ブロックチェーンの承認をお待ち下さい
|
||||
portfolio.pending.step2_buyer.additionalConf=入金は10承認に達しました。\n追加の安全のため、支払いを送信する前に{0}承認を待つことをお勧めします。\n早めに進める場合は自己責任となります。
|
||||
portfolio.pending.step2_buyer.startPayment=支払い開始
|
||||
portfolio.pending.step2_seller.waitPaymentSent=支払いが始まるまでお待ち下さい
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=支払いが到着するまでお待ち下さい
|
||||
|
|
@ -792,6 +793,8 @@ portfolio.pending.support.text.getHelp=問題があれば、トレードチャ
|
|||
portfolio.pending.support.button.getHelp=取引者チャットを開く
|
||||
portfolio.pending.support.headline.halfPeriodOver=支払いを確認
|
||||
portfolio.pending.support.headline.periodOver=トレード期間は終了しました
|
||||
portfolio.pending.support.headline.depositTxMissing=入金トランザクションが見つかりません
|
||||
portfolio.pending.support.depositTxMissing=この取引には入金トランザクションが見つかりません。サポートチケットを開いて、仲裁者に連絡してサポートを受けてください。
|
||||
|
||||
portfolio.pending.mediationRequested=調停は依頼されました
|
||||
portfolio.pending.refundRequested=返金は請求されました
|
||||
|
|
|
|||
|
|
@ -579,6 +579,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Aguardar confirmação da blockchain
|
||||
portfolio.pending.step2_buyer.additionalConf=Depósitos alcançaram 10 confirmações.\nPara maior segurança, recomendamos aguardar {0} confirmações antes de enviar o pagamento.\nProssiga antecipadamente por sua própria conta e risco.
|
||||
portfolio.pending.step2_buyer.startPayment=Iniciar pagamento
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Aguardar início do pagamento
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar recebimento do pagamento
|
||||
|
|
@ -794,6 +795,8 @@ portfolio.pending.support.text.getHelp=Caso tenha problemas, você pode tentar c
|
|||
portfolio.pending.support.button.getHelp=Abrir Chat de Negociante
|
||||
portfolio.pending.support.headline.halfPeriodOver=Verifique o pagamento
|
||||
portfolio.pending.support.headline.periodOver=O período de negociação acabou
|
||||
portfolio.pending.support.headline.depositTxMissing=Transação de depósito ausente
|
||||
portfolio.pending.support.depositTxMissing=Está faltando uma transação de depósito para esta negociação. Abra um ticket de suporte para contatar um árbitro para assistência.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediação requerida
|
||||
portfolio.pending.refundRequested=Reembolso requerido
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Esperando confirmação da blockchain
|
||||
portfolio.pending.step2_buyer.additionalConf=Os depósitos alcançaram 10 confirmações.\nPara maior segurança, recomendamos aguardar {0} confirmações antes de enviar o pagamento.\nProceda antecipadamente por sua própria conta e risco.
|
||||
portfolio.pending.step2_buyer.startPayment=Iniciar pagamento
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Aguardar até que o pagamento inicie
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Aguardar até que o pagamento chegue
|
||||
|
|
@ -791,6 +792,8 @@ portfolio.pending.support.text.getHelp=Se tiver algum problema você pode tentar
|
|||
portfolio.pending.support.button.getHelp=Open Trader Chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Verificar o pagamento
|
||||
portfolio.pending.support.headline.periodOver=O período de negócio acabou
|
||||
portfolio.pending.support.headline.depositTxMissing=Transação de depósito ausente
|
||||
portfolio.pending.support.depositTxMissing=Está faltando uma transação de depósito para esta negociação. Abra um ticket de suporte para contatar um árbitro para assistência.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediação solicitada
|
||||
portfolio.pending.refundRequested=Reembolso pedido
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Ожидание подтверждения в блокчейне
|
||||
portfolio.pending.step2_buyer.additionalConf=Депозиты достигли 10 подтверждений.\nДля дополнительной безопасности мы рекомендуем дождаться {0} подтверждений перед отправкой платежа.\nРанее действия осуществляются на ваш страх и риск.
|
||||
portfolio.pending.step2_buyer.startPayment=Сделать платеж
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Дождитесь начала платежа
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Дождитесь получения платежа
|
||||
|
|
@ -791,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c
|
|||
portfolio.pending.support.button.getHelp=Open Trader Chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Check payment
|
||||
portfolio.pending.support.headline.periodOver=Время сделки истекло
|
||||
portfolio.pending.support.headline.depositTxMissing=Отсутствует депозитная транзакция
|
||||
portfolio.pending.support.depositTxMissing=Для этой сделки отсутствует депозитная транзакция. Откройте тикет в службу поддержки, чтобы связаться с арбитром для получения помощи.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediation requested
|
||||
portfolio.pending.refundRequested=Refund requested
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=รอการยืนยันของบล็อกเชน
|
||||
portfolio.pending.step2_buyer.additionalConf=ยอดฝากถึง 10 การยืนยันแล้ว\nเพื่อความปลอดภัยเพิ่มเติม เราแนะนำให้รอ {0} การยืนยันก่อนทำการชำระเงิน\nดำเนินการล่วงหน้าตามความเสี่ยงของคุณเอง
|
||||
portfolio.pending.step2_buyer.startPayment=เริ่มการชำระเงิน
|
||||
portfolio.pending.step2_seller.waitPaymentSent=รอจนกว่าการชำระเงินจะเริ่มขึ้น
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=รอจนกว่าจะถึงการชำระเงิน
|
||||
|
|
@ -791,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c
|
|||
portfolio.pending.support.button.getHelp=Open Trader Chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Check payment
|
||||
portfolio.pending.support.headline.periodOver=Trade period is over
|
||||
portfolio.pending.support.headline.depositTxMissing=การฝากธุรกรรมหายไป
|
||||
portfolio.pending.support.depositTxMissing=รายการฝากสำหรับการซื้อขายนี้หายไป กรุณาเปิดตั๋วสนับสนุนเพื่อติดต่อผู้ตัดสินเพื่อขอความช่วยเหลือ
|
||||
|
||||
portfolio.pending.mediationRequested=Mediation requested
|
||||
portfolio.pending.refundRequested=Refund requested
|
||||
|
|
|
|||
|
|
@ -623,6 +623,7 @@ portfolio.pending.unconfirmedTooLong=İşlem {0} üzerindeki güvence işlemleri
|
|||
Sorun devam ederse, Haveno desteğiyle iletişime geçin [HYPERLINK:https://matrix.to/#/#haveno:monero.social].
|
||||
|
||||
portfolio.pending.step1.waitForConf=Blok zinciri onaylarını bekleyin
|
||||
portfolio.pending.step2_buyer.additionalConf=Mevduatlar 10 onayı ulaştı.\nEkstra güvenlik için, ödeme göndermeden önce {0} onayı beklemenizi öneririz.\nErken ilerlemek kendi riskinizdedir.
|
||||
portfolio.pending.step2_buyer.startPayment=Ödemeyi başlat
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Ödeme gönderilene kadar bekle
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Ödeme gelene kadar bekle
|
||||
|
|
@ -899,6 +900,8 @@ portfolio.pending.support.headline.getHelp=Yardıma mı ihtiyacınız var?
|
|||
portfolio.pending.support.button.getHelp=Tüccar Sohbetini Aç
|
||||
portfolio.pending.support.headline.halfPeriodOver=Ödemeyi kontrol edin
|
||||
portfolio.pending.support.headline.periodOver=Ticaret süresi doldu
|
||||
portfolio.pending.support.headline.depositTxMissing=Eksik yatırma işlemi
|
||||
portfolio.pending.support.depositTxMissing=Bu işlem için bir para yatırma işlemi eksik. Yardım almak için bir tahkimciyle iletişime geçmek üzere bir destek talebi açın.
|
||||
|
||||
portfolio.pending.arbitrationRequested=Arabuluculuk talep edildi
|
||||
portfolio.pending.mediationRequested=Arabuluculuk talep edildi
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=Đợi xác nhận blockchain
|
||||
portfolio.pending.step2_buyer.additionalConf=Tiền gửi đã đạt 10 xác nhận.\nĐể tăng cường bảo mật, chúng tôi khuyên bạn chờ {0} xác nhận trước khi gửi thanh toán.\nTiến hành sớm là rủi ro của bạn.
|
||||
portfolio.pending.step2_buyer.startPayment=Bắt đầu thanh toán
|
||||
portfolio.pending.step2_seller.waitPaymentSent=Đợi đến khi bắt đầu thanh toán
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=Đợi đến khi khoản thanh toán đến
|
||||
|
|
@ -791,6 +792,8 @@ portfolio.pending.support.text.getHelp=If you have any problems you can try to c
|
|||
portfolio.pending.support.button.getHelp=Open Trader Chat
|
||||
portfolio.pending.support.headline.halfPeriodOver=Check payment
|
||||
portfolio.pending.support.headline.periodOver=Trade period is over
|
||||
portfolio.pending.support.headline.depositTxMissing=Thiếu giao dịch ký quỹ
|
||||
portfolio.pending.support.depositTxMissing=Giao dịch gửi tiền cho thương vụ này bị thiếu. Mở phiếu hỗ trợ để liên hệ với trọng tài để được trợ giúp.
|
||||
|
||||
portfolio.pending.mediationRequested=Mediation requested
|
||||
portfolio.pending.refundRequested=Refund requested
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ portfolio.closedTrades.deviation.help=与市场价格偏差百分比
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=等待区块链确认
|
||||
portfolio.pending.step2_buyer.additionalConf=存款已达到 10 个确认。\n为了额外安全,我们建议在发送付款前等待 {0} 个确认。\n提前操作风险自负。
|
||||
portfolio.pending.step2_buyer.startPayment=开始付款
|
||||
portfolio.pending.step2_seller.waitPaymentSent=等待直到付款
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到达
|
||||
|
|
@ -792,6 +793,8 @@ portfolio.pending.support.text.getHelp=如果您有任何问题,您可以尝
|
|||
portfolio.pending.support.button.getHelp=开启交易聊天
|
||||
portfolio.pending.support.headline.halfPeriodOver=确认付款
|
||||
portfolio.pending.support.headline.periodOver=交易期结束
|
||||
portfolio.pending.support.headline.depositTxMissing=缺少存款交易
|
||||
portfolio.pending.support.depositTxMissing=此交易缺少存款交易。请提交支持工单以联系仲裁员寻求帮助。
|
||||
|
||||
portfolio.pending.mediationRequested=已请求调解员协助
|
||||
portfolio.pending.refundRequested=已请求退款
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ portfolio.closedTrades.deviation.help=Percentage price deviation from market
|
|||
portfolio.pending.invalidTx=There is an issue with a missing or invalid transaction.\n\nPlease do NOT send the traditional or crypto payment.\n\nOpen a support ticket to get assistance from a Mediator.\n\nError message: {0}
|
||||
|
||||
portfolio.pending.step1.waitForConf=等待區塊鏈確認
|
||||
portfolio.pending.step2_buyer.additionalConf=存款已達 10 次確認。\n為了額外安全,我們建議在發送付款前等待 {0} 次確認。\n提前操作風險自負。
|
||||
portfolio.pending.step2_buyer.startPayment=開始付款
|
||||
portfolio.pending.step2_seller.waitPaymentSent=等待直到付款
|
||||
portfolio.pending.step3_buyer.waitPaymentArrived=等待直到付款到達
|
||||
|
|
@ -792,6 +793,8 @@ portfolio.pending.support.text.getHelp=如果您有任何問題,您可以嘗
|
|||
portfolio.pending.support.button.getHelp=開啟交易聊天
|
||||
portfolio.pending.support.headline.halfPeriodOver=確認付款
|
||||
portfolio.pending.support.headline.periodOver=交易期結束
|
||||
portfolio.pending.support.headline.depositTxMissing=缺少存款交易
|
||||
portfolio.pending.support.depositTxMissing=此交易缺少存款。請開啟支援工單以聯絡仲裁者協助處理。
|
||||
|
||||
portfolio.pending.mediationRequested=已請求調解員協助
|
||||
portfolio.pending.refundRequested=已請求退款
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
package haveno.desktop.main;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.app.DevEnv;
|
||||
|
|
@ -219,47 +221,49 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
|||
(a, b) -> a && b);
|
||||
tradesAndUIReady.subscribe((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
tradeManager.applyTradePeriodState();
|
||||
ThreadUtils.submitToPool(() -> {
|
||||
tradeManager.applyTradePeriodState();
|
||||
|
||||
tradeManager.getOpenTrades().forEach(trade -> {
|
||||
tradeManager.getOpenTrades().forEach(trade -> {
|
||||
|
||||
// check initialization error
|
||||
if (trade.getInitError() != null) {
|
||||
new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" +
|
||||
trade.getInitError().getMessage())
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
// check initialization error
|
||||
if (trade.getInitError() != null) {
|
||||
new Popup().warning("Error initializing trade" + " " + trade.getShortId() + "\n\n" +
|
||||
trade.getInitError().getMessage())
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// check trade period
|
||||
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
|
||||
String key;
|
||||
switch (trade.getPeriodState()) {
|
||||
case FIRST_HALF:
|
||||
break;
|
||||
case SECOND_HALF:
|
||||
key = "displayHalfTradePeriodOver" + trade.getId();
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
DontShowAgainLookup.dontShowAgain(key, true);
|
||||
if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade
|
||||
new Popup().warning(Res.get("popup.warning.tradePeriod.halfReached",
|
||||
trade.getShortId(),
|
||||
DisplayUtils.formatDateTime(maxTradePeriodDate)))
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
case TRADE_PERIOD_OVER:
|
||||
key = "displayTradePeriodOver" + trade.getId();
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
DontShowAgainLookup.dontShowAgain(key, true);
|
||||
if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade
|
||||
new Popup().warning(Res.get("popup.warning.tradePeriod.ended",
|
||||
trade.getShortId(),
|
||||
DisplayUtils.formatDateTime(maxTradePeriodDate)))
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
// check trade period
|
||||
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
|
||||
String key;
|
||||
switch (trade.getPeriodState()) {
|
||||
case FIRST_HALF:
|
||||
break;
|
||||
case SECOND_HALF:
|
||||
key = "displayHalfTradePeriodOver" + trade.getId();
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
DontShowAgainLookup.dontShowAgain(key, true);
|
||||
if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade
|
||||
new Popup().warning(Res.get("popup.warning.tradePeriod.halfReached",
|
||||
trade.getShortId(),
|
||||
DisplayUtils.formatDateTime(maxTradePeriodDate)))
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
case TRADE_PERIOD_OVER:
|
||||
key = "displayTradePeriodOver" + trade.getId();
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
DontShowAgainLookup.dontShowAgain(key, true);
|
||||
if (trade instanceof ArbitratorTrade) break; // skip popup if arbitrator trade
|
||||
new Popup().warning(Res.get("popup.warning.tradePeriod.ended",
|
||||
trade.getShortId(),
|
||||
DisplayUtils.formatDateTime(maxTradePeriodDate)))
|
||||
.show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -170,10 +170,6 @@ public class TransactionsListItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (amount.compareTo(BigInteger.ZERO) == 0) {
|
||||
details = Res.get("funds.tx.noFundsFromDispute");
|
||||
}
|
||||
}
|
||||
|
||||
// get tx date/time
|
||||
|
|
|
|||
|
|
@ -205,8 +205,12 @@ public class NotificationCenter {
|
|||
message = Res.get("notification.trade.accepted", role);
|
||||
}
|
||||
|
||||
if (trade instanceof BuyerTrade && phase.ordinal() == Trade.Phase.DEPOSITS_UNLOCKED.ordinal())
|
||||
message = Res.get("notification.trade.unlocked");
|
||||
if (trade instanceof BuyerTrade) {
|
||||
if (phase.ordinal() == Trade.Phase.DEPOSITS_UNLOCKED.ordinal())
|
||||
message = Res.get("notification.trade.unlocked");
|
||||
else if (phase.ordinal() == Trade.Phase.DEPOSITS_FINALIZED.ordinal())
|
||||
message = Res.get("notification.trade.finalized", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED);
|
||||
}
|
||||
else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.PAYMENT_SENT.ordinal())
|
||||
message = Res.get("notification.trade.paymentSent");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,6 +225,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
} else if (trade.isPayoutPublished()) {
|
||||
log.warn("Payout is already published for {} {}, disabling payout controls", trade.getClass().getSimpleName(), trade.getId());
|
||||
disableTradeAmountPayoutControls();
|
||||
} else if (trade.isDepositTxMissing()) {
|
||||
log.warn("Missing deposit tx for {} {}, disabling some payout controls", trade.getClass().getSimpleName(), trade.getId());
|
||||
disableTradeAmountPayoutControlsWhenDepositMissing();
|
||||
}
|
||||
|
||||
setReasonRadioButtonState();
|
||||
|
|
@ -259,6 +262,11 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
reasonWasTradeAlreadySettledRadioButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void disableTradeAmountPayoutControlsWhenDepositMissing() {
|
||||
buyerGetsTradeAmountRadioButton.setDisable(true);
|
||||
sellerGetsTradeAmountRadioButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void addInfoPane() {
|
||||
Contract contract = dispute.getContract();
|
||||
addTitledGroupBg(gridPane, ++rowIndex, 17, Res.get("disputeSummaryWindow.title")).getStyleClass().add("last");
|
||||
|
|
@ -374,10 +382,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
return;
|
||||
}
|
||||
|
||||
Contract contract = dispute.getContract();
|
||||
BigInteger available = contract.getTradeAmount()
|
||||
.add(trade.getBuyer().getSecurityDeposit())
|
||||
.add(trade.getSeller().getSecurityDeposit());
|
||||
BigInteger available = trade.getWallet().getBalance();
|
||||
BigInteger enteredAmount = HavenoUtils.parseXmr(inputTextField.getText());
|
||||
if (enteredAmount.compareTo(available) > 0) {
|
||||
enteredAmount = available;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import haveno.desktop.util.GUIUtil;
|
|||
import haveno.network.p2p.P2PService;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
|
|
@ -107,6 +109,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
private Subscription messageStateSubscription;
|
||||
@Getter
|
||||
protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty();
|
||||
private transient Map<String, Boolean> showPaymentDetailsEarly = new HashMap<String, Boolean>();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -266,7 +269,13 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
return getMaxTradePeriodDate() != null && new Date().after(getMaxTradePeriodDate());
|
||||
}
|
||||
|
||||
//
|
||||
public boolean getShowPaymentDetailsEarly() {
|
||||
return showPaymentDetailsEarly.getOrDefault(dataModel.getTrade().getId(), false);
|
||||
}
|
||||
|
||||
public void setShowPaymentDetailsEarly(boolean show) {
|
||||
showPaymentDetailsEarly.put(dataModel.getTrade().getId(), show);
|
||||
}
|
||||
|
||||
String getMyRole(PendingTradesListItem item) {
|
||||
return tradeUtil.getRole(item.getTrade());
|
||||
|
|
@ -349,7 +358,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void onTradeStateChanged(Trade.State tradeState) {
|
||||
log.info("UI tradeState={}, id={}",
|
||||
log.debug("UI tradeState={}, id={}",
|
||||
tradeState,
|
||||
trade != null ? trade.getShortId() : "trade is null");
|
||||
|
||||
|
|
@ -391,8 +400,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
break;
|
||||
|
||||
// buyer and seller step 2
|
||||
// deposits unlocked
|
||||
// deposits unlocked or finalized
|
||||
case DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN:
|
||||
case DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN:
|
||||
buyerState.set(BuyerState.STEP2);
|
||||
sellerState.set(SellerState.STEP2);
|
||||
break;
|
||||
|
|
@ -443,7 +453,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
}
|
||||
|
||||
private void onPayoutStateChanged(Trade.PayoutState payoutState) {
|
||||
log.info("UI payoutState={}, id={}",
|
||||
log.debug("UI payoutState={}, id={}",
|
||||
payoutState,
|
||||
trade != null ? trade.getShortId() : "trade is null");
|
||||
|
||||
|
|
@ -453,6 +463,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
case PAYOUT_PUBLISHED:
|
||||
case PAYOUT_CONFIRMED:
|
||||
case PAYOUT_UNLOCKED:
|
||||
case PAYOUT_FINALIZED:
|
||||
sellerState.set(SellerState.STEP4);
|
||||
buyerState.set(BuyerState.STEP4);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ public class TradeStepInfo {
|
|||
IN_REFUND_REQUEST_PEER_REQUESTED,
|
||||
WARN_HALF_PERIOD,
|
||||
WARN_PERIOD_OVER,
|
||||
DEPOSIT_MISSING,
|
||||
TRADE_COMPLETED
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ public class TradeStepInfo {
|
|||
private State state = State.UNDEFINED;
|
||||
private Supplier<String> firstHalfOverWarnTextSupplier = () -> "";
|
||||
private Supplier<String> periodOverWarnTextSupplier = () -> "";
|
||||
private Supplier<String> depositTxMissingWarnTextSupplier = () -> "";
|
||||
|
||||
TradeStepInfo(TitledGroupBg titledGroupBg,
|
||||
SimpleMarkdownLabel label,
|
||||
|
|
@ -95,6 +97,10 @@ public class TradeStepInfo {
|
|||
this.periodOverWarnTextSupplier = periodOverWarnTextSupplier;
|
||||
}
|
||||
|
||||
public void setDepositTxMissingWarnTextSupplier(Supplier<String> depositTxMissingWarnTextSupplier) {
|
||||
this.depositTxMissingWarnTextSupplier = depositTxMissingWarnTextSupplier;
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
this.state = state;
|
||||
switch (state) {
|
||||
|
|
@ -192,12 +198,23 @@ public class TradeStepInfo {
|
|||
button.getStyleClass().remove("action-button");
|
||||
button.setDisable(false);
|
||||
break;
|
||||
case DEPOSIT_MISSING:
|
||||
// red button
|
||||
titledGroupBg.setText(Res.get("portfolio.pending.support.headline.depositTxMissing"));
|
||||
label.updateContent(depositTxMissingWarnTextSupplier.get());
|
||||
button.setText(Res.get("portfolio.pending.openSupport").toUpperCase());
|
||||
button.setId("open-dispute-button");
|
||||
button.getStyleClass().remove("action-button");
|
||||
button.setDisable(false);
|
||||
break;
|
||||
case TRADE_COMPLETED:
|
||||
// hide group
|
||||
titledGroupBg.setVisible(false);
|
||||
label.setVisible(false);
|
||||
button.setVisible(false);
|
||||
footerLabel.setVisible(false);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (trade != null && trade.getPayoutTxId() != null) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import haveno.core.trade.HavenoUtils;
|
|||
import haveno.core.trade.MakerTrade;
|
||||
import haveno.core.trade.TakerTrade;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.user.DontShowAgainLookup;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.desktop.components.InfoTextField;
|
||||
import haveno.desktop.components.TitledGroupBg;
|
||||
|
|
@ -50,8 +49,6 @@ import static haveno.desktop.util.FormBuilder.addTitledGroupBg;
|
|||
import static haveno.desktop.util.FormBuilder.addTopLabelTxIdTextField;
|
||||
import haveno.desktop.util.Layout;
|
||||
import haveno.network.p2p.BootstrapListener;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
|
|
@ -80,7 +77,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
protected final Preferences preferences;
|
||||
protected final GridPane gridPane;
|
||||
|
||||
private Subscription tradePeriodStateSubscription, disputeStateSubscription, mediationResultStateSubscription;
|
||||
private Subscription tradePeriodStateSubscription, tradeStateSubscription, disputeStateSubscription, mediationResultStateSubscription;
|
||||
protected int gridRow = 0;
|
||||
private TextField timeLeftTextField;
|
||||
private ProgressBar timeLeftProgressBar;
|
||||
|
|
@ -144,8 +141,10 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
addContent();
|
||||
|
||||
errorMessageListener = (observable, oldValue, newValue) -> {
|
||||
if (newValue != null)
|
||||
if (newValue != null) {
|
||||
log.warn("Showing popup for trade error {} {}", trade.getClass().getSimpleName(), trade.getId(), new RuntimeException(newValue));
|
||||
new Popup().error(newValue).show();
|
||||
}
|
||||
};
|
||||
|
||||
clockListener = new ClockWatcher.Listener() {
|
||||
|
|
@ -192,7 +191,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
trade.errorMessageProperty().addListener(errorMessageListener);
|
||||
|
||||
tradeStepInfo.setOnAction(e -> {
|
||||
if (!isArbitrationOpenedState() && this.isTradePeriodOver()) {
|
||||
if (!isArbitrationOpenedState() && (this.isTradePeriodOver() || trade.isDepositTxMissing())) {
|
||||
openSupportTicket();
|
||||
} else {
|
||||
openChat();
|
||||
|
|
@ -258,15 +257,28 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
}
|
||||
});
|
||||
|
||||
if (trade.wasWalletPolled.get()) addTradeStateSubscription();
|
||||
else trade.wasWalletPolled.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) addTradeStateSubscription();
|
||||
});
|
||||
|
||||
UserThread.execute(() -> model.p2PService.removeP2PServiceListener(bootstrapListener));
|
||||
}
|
||||
|
||||
private void addTradeStateSubscription() {
|
||||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> {
|
||||
if (newValue != null) {
|
||||
UserThread.execute(() -> updateTradeState(newValue));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openSupportTicket() {
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) {
|
||||
new Popup().warning(Res.get("portfolio.pending.error.depositTxNotConfirmed")).show();
|
||||
} else {
|
||||
if (trade.isDepositTxMissing() || trade.getPhase().ordinal() >= Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) {
|
||||
applyOnDisputeOpened();
|
||||
model.dataModel.onOpenDispute();
|
||||
} else {
|
||||
new Popup().warning(Res.get("portfolio.pending.error.depositTxNotConfirmed")).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +311,8 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
|
||||
if (tradePeriodStateSubscription != null)
|
||||
tradePeriodStateSubscription.unsubscribe();
|
||||
if (tradeStateSubscription != null)
|
||||
tradeStateSubscription.unsubscribe();
|
||||
|
||||
if (clockListener != null)
|
||||
model.clockWatcher.removeListener(clockListener);
|
||||
|
|
@ -447,6 +461,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
|
||||
tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText);
|
||||
tradeStepInfo.setPeriodOverWarnTextSupplier(this::getPeriodOverWarnText);
|
||||
tradeStepInfo.setDepositTxMissingWarnTextSupplier(this::getDepositTxMissingWarnText);
|
||||
}
|
||||
|
||||
protected void hideTradeStepInfo() {
|
||||
|
|
@ -466,6 +481,10 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
return "";
|
||||
}
|
||||
|
||||
protected String getDepositTxMissingWarnText() {
|
||||
return Res.get("portfolio.pending.support.depositTxMissing");
|
||||
}
|
||||
|
||||
protected void applyOnDisputeOpened() {
|
||||
}
|
||||
|
||||
|
|
@ -782,34 +801,40 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
}
|
||||
}
|
||||
|
||||
// private void checkIfLockTimeIsOver() {
|
||||
// if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) {
|
||||
// Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
|
||||
// if (delayedPayoutTx != null) {
|
||||
// long lockTime = delayedPayoutTx.getLockTime();
|
||||
// int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight();
|
||||
// long remaining = lockTime - bestChainHeight;
|
||||
// if (remaining <= 0) {
|
||||
// openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId()));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
protected void checkForUnconfirmedTimeout() {
|
||||
if (trade.isDepositsConfirmed()) return;
|
||||
long unconfirmedHours = Duration.between(trade.getDate().toInstant(), Instant.now()).toHours();
|
||||
if (unconfirmedHours >= 3 && !trade.hasFailed()) {
|
||||
String key = "tradeUnconfirmedTooLong_" + trade.getShortId();
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup().warning(Res.get("portfolio.pending.unconfirmedTooLong", trade.getShortId(), unconfirmedHours))
|
||||
.dontShowAgainId(key)
|
||||
.closeButtonText(Res.get("shared.ok"))
|
||||
.show();
|
||||
}
|
||||
private void updateTradeState(Trade.State tradeState) {
|
||||
if (!trade.getDisputeState().isOpen() && trade.isDepositTxMissing()) {
|
||||
tradeStepInfo.setState(TradeStepInfo.State.DEPOSIT_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
// private void checkIfLockTimeIsOver() {
|
||||
// if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) {
|
||||
// Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
|
||||
// if (delayedPayoutTx != null) {
|
||||
// long lockTime = delayedPayoutTx.getLockTime();
|
||||
// int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight();
|
||||
// long remaining = lockTime - bestChainHeight;
|
||||
// if (remaining <= 0) {
|
||||
// openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId()));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// protected void checkForUnconfirmedTimeout() {
|
||||
// if (trade.isDepositsConfirmed()) return;
|
||||
// long unconfirmedHours = Duration.between(trade.getDate().toInstant(), Instant.now()).toHours();
|
||||
// if (unconfirmedHours >= 3 && !trade.hasFailed()) {
|
||||
// String key = "tradeUnconfirmedTooLong_" + trade.getShortId();
|
||||
// if (DontShowAgainLookup.showAgain(key)) {
|
||||
// new Popup().warning(Res.get("portfolio.pending.unconfirmedTooLong", trade.getShortId(), unconfirmedHours))
|
||||
// .dontShowAgainId(key)
|
||||
// .closeButtonText(Res.get("shared.ok"))
|
||||
// .show();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// TradeDurationLimitInfo
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class BuyerStep1View extends TradeStepView {
|
|||
super.onPendingTradesInitialized();
|
||||
//validatePayoutTx(); // TODO (woodser): no payout tx in xmr integration, do something else?
|
||||
//validateDepositInputs();
|
||||
checkForUnconfirmedTimeout();
|
||||
//checkForUnconfirmedTimeout();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,12 @@ import javafx.geometry.Insets;
|
|||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
|
|
@ -129,6 +132,9 @@ public class BuyerStep2View extends TradeStepView {
|
|||
private BusyAnimation busyAnimation;
|
||||
private Subscription tradeStatePropertySubscription;
|
||||
private Timer timeoutTimer;
|
||||
private int paymentAccountGridRow = 0;
|
||||
private GridPane paymentAccountGridPane;
|
||||
private GridPane moreConfirmationsGridPane;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, Initialisation
|
||||
|
|
@ -224,202 +230,216 @@ public class BuyerStep2View extends TradeStepView {
|
|||
gridPane.getColumnConstraints().get(1).setHgrow(Priority.ALWAYS);
|
||||
|
||||
addTradeInfoBlock();
|
||||
createPaymentDetailsGridPane();
|
||||
createRecommendationGridPane();
|
||||
|
||||
// attach grid pane based on current state
|
||||
EasyBind.subscribe(trade.statePhaseProperty(), newValue -> {
|
||||
if (trade.isDepositsFinalized() || trade.isPaymentSent() || model.getShowPaymentDetailsEarly()) {
|
||||
attachPaymentDetailsGrid();
|
||||
} else {
|
||||
attachRecommendationGrid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createPaymentDetailsGridPane() {
|
||||
PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
|
||||
String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "<pending>";
|
||||
TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4,
|
||||
|
||||
paymentAccountGridPane = createGridPane();
|
||||
TitledGroupBg accountTitledGroupBg = addTitledGroupBg(paymentAccountGridPane, paymentAccountGridRow, 4,
|
||||
Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)),
|
||||
Layout.COMPACT_GROUP_DISTANCE);
|
||||
TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 0,
|
||||
TextFieldWithCopyIcon field = addTopLabelTextFieldWithCopyIcon(paymentAccountGridPane, paymentAccountGridRow, 0,
|
||||
Res.get("portfolio.pending.step2_buyer.amountToTransfer"),
|
||||
model.getFiatVolume(),
|
||||
Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE).second;
|
||||
field.setCopyWithoutCurrencyPostFix(true);
|
||||
|
||||
//preland: this fixes a textarea layout glitch
|
||||
//preland: this fixes a textarea layout glitch // TODO: can this be removed now?
|
||||
TextArea uiHack = new TextArea();
|
||||
uiHack.setMaxHeight(1);
|
||||
GridPane.setRowIndex(uiHack, 1);
|
||||
GridPane.setMargin(uiHack, new Insets(0, 0, 0, 0));
|
||||
uiHack.setVisible(false);
|
||||
gridPane.getChildren().add(uiHack);
|
||||
paymentAccountGridPane.getChildren().add(uiHack);
|
||||
|
||||
switch (paymentMethodId) {
|
||||
case PaymentMethod.UPHOLD_ID:
|
||||
gridRow = UpholdForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = UpholdForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.MONEY_BEAM_ID:
|
||||
gridRow = MoneyBeamForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = MoneyBeamForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.POPMONEY_ID:
|
||||
gridRow = PopmoneyForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PopmoneyForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.REVOLUT_ID:
|
||||
gridRow = RevolutForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = RevolutForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PERFECT_MONEY_ID:
|
||||
gridRow = PerfectMoneyForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PerfectMoneyForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SEPA_ID:
|
||||
gridRow = SepaForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = SepaForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SEPA_INSTANT_ID:
|
||||
gridRow = SepaInstantForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = SepaInstantForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.FASTER_PAYMENTS_ID:
|
||||
gridRow = FasterPaymentsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = FasterPaymentsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.NATIONAL_BANK_ID:
|
||||
gridRow = NationalBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = NationalBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.AUSTRALIA_PAYID_ID:
|
||||
gridRow = AustraliaPayidForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = AustraliaPayidForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SAME_BANK_ID:
|
||||
gridRow = SameBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = SameBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SPECIFIC_BANKS_ID:
|
||||
gridRow = SpecificBankForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = SpecificBankForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SWISH_ID:
|
||||
gridRow = SwishForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = SwishForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.ALI_PAY_ID:
|
||||
gridRow = AliPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = AliPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.WECHAT_PAY_ID:
|
||||
gridRow = WeChatPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = WeChatPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.ZELLE_ID:
|
||||
gridRow = ZelleForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = ZelleForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.CHASE_QUICK_PAY_ID:
|
||||
gridRow = ChaseQuickPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = ChaseQuickPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.INTERAC_E_TRANSFER_ID:
|
||||
gridRow = InteracETransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = InteracETransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.JAPAN_BANK_ID:
|
||||
gridRow = JapanBankTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = JapanBankTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.US_POSTAL_MONEY_ORDER_ID:
|
||||
gridRow = USPostalMoneyOrderForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = USPostalMoneyOrderForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.CASH_DEPOSIT_ID:
|
||||
gridRow = CashDepositForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = CashDepositForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PAY_BY_MAIL_ID:
|
||||
gridRow = PayByMailForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PayByMailForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.CASH_AT_ATM_ID:
|
||||
gridRow = CashAtAtmForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = CashAtAtmForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.MONEY_GRAM_ID:
|
||||
gridRow = MoneyGramForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = MoneyGramForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.WESTERN_UNION_ID:
|
||||
gridRow = WesternUnionForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = WesternUnionForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.HAL_CASH_ID:
|
||||
gridRow = HalCashForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = HalCashForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.F2F_ID:
|
||||
checkNotNull(model.dataModel.getTrade(), "model.dataModel.getTrade() must not be null");
|
||||
checkNotNull(model.dataModel.getTrade().getOffer(), "model.dataModel.getTrade().getOffer() must not be null");
|
||||
gridRow = F2FForm.addStep2Form(gridPane, gridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true);
|
||||
paymentAccountGridRow = F2FForm.addStep2Form(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, model.dataModel.getTrade().getOffer(), 0, true);
|
||||
break;
|
||||
case PaymentMethod.BLOCK_CHAINS_ID:
|
||||
case PaymentMethod.BLOCK_CHAINS_INSTANT_ID:
|
||||
String labelTitle = Res.get("portfolio.pending.step2_buyer.sellersAddress", getCurrencyName(trade));
|
||||
gridRow = AssetsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, labelTitle);
|
||||
paymentAccountGridRow = AssetsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, labelTitle);
|
||||
break;
|
||||
case PaymentMethod.PROMPT_PAY_ID:
|
||||
gridRow = PromptPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PromptPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.ADVANCED_CASH_ID:
|
||||
gridRow = AdvancedCashForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = AdvancedCashForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.TRANSFERWISE_ID:
|
||||
gridRow = TransferwiseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = TransferwiseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.TRANSFERWISE_USD_ID:
|
||||
gridRow = TransferwiseUsdForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = TransferwiseUsdForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PAYSERA_ID:
|
||||
gridRow = PayseraForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PayseraForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PAXUM_ID:
|
||||
gridRow = PaxumForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PaxumForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.NEFT_ID:
|
||||
gridRow = NeftForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = NeftForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.RTGS_ID:
|
||||
gridRow = RtgsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = RtgsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.IMPS_ID:
|
||||
gridRow = ImpsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = ImpsForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.UPI_ID:
|
||||
gridRow = UpiForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = UpiForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PAYTM_ID:
|
||||
gridRow = PaytmForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PaytmForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.NEQUI_ID:
|
||||
gridRow = NequiForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = NequiForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.BIZUM_ID:
|
||||
gridRow = BizumForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = BizumForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PIX_ID:
|
||||
gridRow = PixForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PixForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.AMAZON_GIFT_CARD_ID:
|
||||
gridRow = AmazonGiftCardForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = AmazonGiftCardForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.CAPITUAL_ID:
|
||||
gridRow = CapitualForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = CapitualForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.CELPAY_ID:
|
||||
gridRow = CelPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = CelPayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.MONESE_ID:
|
||||
gridRow = MoneseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = MoneseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SATISPAY_ID:
|
||||
gridRow = SatispayForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = SatispayForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.TIKKIE_ID:
|
||||
gridRow = TikkieForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = TikkieForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.VERSE_ID:
|
||||
gridRow = VerseForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = VerseForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.STRIKE_ID:
|
||||
gridRow = StrikeForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = StrikeForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.SWIFT_ID:
|
||||
gridRow = SwiftForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, trade);
|
||||
paymentAccountGridRow = SwiftForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload, trade);
|
||||
break;
|
||||
case PaymentMethod.ACH_TRANSFER_ID:
|
||||
gridRow = AchTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = AchTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.DOMESTIC_WIRE_TRANSFER_ID:
|
||||
gridRow = DomesticWireTransferForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = DomesticWireTransferForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.CASH_APP_ID:
|
||||
gridRow = CashAppForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = CashAppForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PAYPAL_ID:
|
||||
gridRow = PayPalForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PayPalForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.VENMO_ID:
|
||||
gridRow = VenmoForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = VenmoForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
case PaymentMethod.PAYSAFE_ID:
|
||||
gridRow = PaysafeForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload);
|
||||
paymentAccountGridRow = PaysafeForm.addFormForBuyer(paymentAccountGridPane, paymentAccountGridRow, paymentAccountPayload);
|
||||
break;
|
||||
default:
|
||||
log.error("Not supported PaymentMethod: " + paymentMethodId);
|
||||
|
|
@ -438,19 +458,19 @@ public class BuyerStep2View extends TradeStepView {
|
|||
.findFirst()
|
||||
.ifPresent(paymentAccount -> {
|
||||
String accountName = paymentAccount.getAccountName();
|
||||
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0,
|
||||
addCompactTopLabelTextFieldWithCopyIcon(paymentAccountGridPane, ++paymentAccountGridRow, 0,
|
||||
Res.get("portfolio.pending.step2_buyer.buyerAccount"), accountName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
GridPane.setRowSpan(accountTitledGroupBg, gridRow - 1);
|
||||
GridPane.setRowSpan(accountTitledGroupBg, gridRow + paymentAccountGridRow - 1);
|
||||
|
||||
Tuple4<Button, BusyAnimation, Label, HBox> tuple3 = addButtonBusyAnimationLabel(gridPane, ++gridRow, 0,
|
||||
Tuple4<Button, BusyAnimation, Label, HBox> tuple3 = addButtonBusyAnimationLabel(paymentAccountGridPane, ++paymentAccountGridRow, 0,
|
||||
Res.get("portfolio.pending.step2_buyer.paymentSent"), 10);
|
||||
|
||||
HBox hBox = tuple3.fourth;
|
||||
GridPane.setColumnSpan(hBox, 2);
|
||||
HBox confirmButtonHBox = tuple3.fourth;
|
||||
GridPane.setColumnSpan(confirmButtonHBox, 2);
|
||||
confirmButton = tuple3.first;
|
||||
confirmButton.setDisable(!confirmPaymentSentPermitted());
|
||||
confirmButton.setOnAction(e -> onPaymentSent());
|
||||
|
|
@ -458,6 +478,64 @@ public class BuyerStep2View extends TradeStepView {
|
|||
statusLabel = tuple3.third;
|
||||
}
|
||||
|
||||
private void createRecommendationGridPane() {
|
||||
|
||||
// create grid pane to show recommendation for more blocks
|
||||
moreConfirmationsGridPane = new GridPane();
|
||||
moreConfirmationsGridPane.setStyle("-fx-background-color: -bs-content-background-gray;");
|
||||
moreConfirmationsGridPane.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
|
||||
// add title
|
||||
addTitledGroupBg(moreConfirmationsGridPane, 0, 1, Res.get("portfolio.pending.step1.waitForConf"), Layout.COMPACT_GROUP_DISTANCE);
|
||||
|
||||
// add text
|
||||
Label label = new Label(Res.get("portfolio.pending.step2_buyer.additionalConf", Trade.NUM_BLOCKS_DEPOSITS_FINALIZED));
|
||||
label.setFont(new Font(16));
|
||||
GridPane.setMargin(label, new Insets(20, 0, 0, 0));
|
||||
moreConfirmationsGridPane.add(label, 0, 1, 2, 1);
|
||||
|
||||
// add button to show payment details
|
||||
Button showPaymentDetailsButton = new Button("Show payment details early");
|
||||
showPaymentDetailsButton.getStyleClass().add("action-button");
|
||||
GridPane.setMargin(showPaymentDetailsButton, new Insets(20, 0, 0, 0));
|
||||
showPaymentDetailsButton.setOnAction(e -> {
|
||||
model.setShowPaymentDetailsEarly(true);
|
||||
gridPane.getChildren().remove(moreConfirmationsGridPane);
|
||||
gridPane.getChildren().add(paymentAccountGridPane);
|
||||
GridPane.setRowIndex(paymentAccountGridPane, gridRow + 1);
|
||||
GridPane.setColumnSpan(paymentAccountGridPane, 2);
|
||||
});
|
||||
moreConfirmationsGridPane.add(showPaymentDetailsButton, 0, 2);
|
||||
}
|
||||
|
||||
private GridPane createGridPane() {
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(Layout.GRID_GAP);
|
||||
gridPane.setVgap(Layout.GRID_GAP);
|
||||
ColumnConstraints columnConstraints1 = new ColumnConstraints();
|
||||
columnConstraints1.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints columnConstraints2 = new ColumnConstraints();
|
||||
columnConstraints2.setHgrow(Priority.ALWAYS);
|
||||
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
|
||||
return gridPane;
|
||||
}
|
||||
|
||||
private void attachRecommendationGrid() {
|
||||
if (gridPane.getChildren().contains(moreConfirmationsGridPane)) return;
|
||||
if (gridPane.getChildren().contains(paymentAccountGridPane)) gridPane.getChildren().remove(paymentAccountGridPane);
|
||||
gridPane.getChildren().add(moreConfirmationsGridPane);
|
||||
GridPane.setRowIndex(moreConfirmationsGridPane, gridRow + 1);
|
||||
GridPane.setColumnSpan(moreConfirmationsGridPane, 2);
|
||||
}
|
||||
|
||||
private void attachPaymentDetailsGrid() {
|
||||
if (gridPane.getChildren().contains(paymentAccountGridPane)) return;
|
||||
if (gridPane.getChildren().contains(moreConfirmationsGridPane)) gridPane.getChildren().remove(moreConfirmationsGridPane);
|
||||
gridPane.getChildren().add(paymentAccountGridPane);
|
||||
GridPane.setRowIndex(paymentAccountGridPane, gridRow + 1);
|
||||
GridPane.setColumnSpan(paymentAccountGridPane, 2);
|
||||
}
|
||||
|
||||
private boolean confirmPaymentSentPermitted() {
|
||||
if (!trade.confirmPermitted()) return false;
|
||||
if (trade.getState() == Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) return true;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class SellerStep1View extends TradeStepView {
|
|||
@Override
|
||||
protected void onPendingTradesInitialized() {
|
||||
super.onPendingTradesInitialized();
|
||||
checkForUnconfirmedTimeout();
|
||||
//checkForUnconfirmedTimeout();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -896,11 +896,13 @@ message TradeInfo {
|
|||
bool is_deposits_published = 25;
|
||||
bool is_deposits_confirmed = 26;
|
||||
bool is_deposits_unlocked = 27;
|
||||
bool is_deposits_finalized = 43;
|
||||
bool is_payment_sent = 28;
|
||||
bool is_payment_received = 29;
|
||||
bool is_payout_published = 30;
|
||||
bool is_payout_confirmed = 31;
|
||||
bool is_payout_unlocked = 32;
|
||||
bool is_payout_finalized = 44;
|
||||
bool is_completed = 33;
|
||||
string contract_as_json = 34;
|
||||
ContractInfo contract = 35;
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ message PaymentReceivedMessage {
|
|||
SignedWitness buyer_signed_witness = 9;
|
||||
PaymentSentMessage payment_sent_message = 10;
|
||||
bytes seller_signature = 11;
|
||||
string payout_tx_id = 12;
|
||||
}
|
||||
|
||||
message MediatedPayoutTxPublishedMessage {
|
||||
|
|
@ -1456,6 +1457,7 @@ message Trade {
|
|||
DEPOSIT_TXS_SEEN_IN_NETWORK = 13;
|
||||
DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 14;
|
||||
DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 15;
|
||||
DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN = 28;
|
||||
BUYER_CONFIRMED_PAYMENT_SENT = 16;
|
||||
BUYER_SENT_PAYMENT_SENT_MSG = 17;
|
||||
BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 18;
|
||||
|
|
@ -1477,6 +1479,7 @@ message Trade {
|
|||
DEPOSITS_PUBLISHED = 3;
|
||||
DEPOSITS_CONFIRMED = 4;
|
||||
DEPOSITS_UNLOCKED = 5;
|
||||
DEPOSITS_FINALIZED = 8;
|
||||
PAYMENT_SENT = 6;
|
||||
PAYMENT_RECEIVED = 7;
|
||||
}
|
||||
|
|
@ -1486,6 +1489,7 @@ message Trade {
|
|||
PAYOUT_PUBLISHED = 1;
|
||||
PAYOUT_CONFIRMED = 2;
|
||||
PAYOUT_UNLOCKED = 3;
|
||||
PAYOUT_FINALIZED = 4;
|
||||
}
|
||||
|
||||
enum DisputeState {
|
||||
|
|
@ -1585,6 +1589,8 @@ message ProcessModel {
|
|||
int64 trade_protocol_error_height = 18;
|
||||
string trade_fee_address = 19;
|
||||
bool import_multisig_hex_scheduled = 20;
|
||||
bool payment_sent_payout_tx_stale = 21;
|
||||
bool error_on_payment_received_msg = 22 [deprecated = true]; // used during debugging across clients (can be repurposed)
|
||||
}
|
||||
|
||||
message TradePeer {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue