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:
woodser 2025-09-14 07:49:45 -04:00 committed by GitHub
parent 7fa633273c
commit 35418e5290
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1474 additions and 623 deletions

View file

@ -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) \

View file

@ -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: {}" +

View file

@ -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());

View file

@ -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 {

View file

@ -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" +

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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. " +

View file

@ -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. " +

View file

@ -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);
}

View file

@ -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

View file

@ -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,

View file

@ -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();
}
}

View file

@ -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());

View file

@ -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)

View file

@ -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()));

View file

@ -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());

View file

@ -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

View file

@ -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);
}

View file

@ -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;

View file

@ -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

View file

@ -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 {

View file

@ -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());

View file

@ -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;
}
}

View file

@ -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() {

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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}.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 dattendre {0} confirmations avant denvoyer 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 laide.
portfolio.pending.mediationRequested=Médiation demandée
portfolio.pending.refundRequested=Remboursement demandé

View file

@ -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

View file

@ -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=返金は請求されました

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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=已请求退款

View file

@ -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=已請求退款

View file

@ -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;
}
});
});
}
});

View file

@ -170,10 +170,6 @@ public class TransactionsListItem {
}
}
}
} else {
if (amount.compareTo(BigInteger.ZERO) == 0) {
details = Res.get("funds.tx.noFundsFromDispute");
}
}
// get tx date/time

View file

@ -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");
}

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -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();
}

View file

@ -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;

View file

@ -34,7 +34,7 @@ public class SellerStep1View extends TradeStepView {
@Override
protected void onPendingTradesInitialized() {
super.onPendingTradesInitialized();
checkForUnconfirmedTimeout();
//checkForUnconfirmedTimeout();
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -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;

View file

@ -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 {