mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04:00
reprocess payout messages on error to improve resilience
reprocess on curved schedule, restart, or connection change invalid messages are nacked using IllegalArgumentException disputes are considered open by ack on chat message don't show trade completion screen until payout published cannot confirm payment sent/received while disconnected from monerod add operation manual w/ instructions to manually open dispute close account before deletion fix popup with error "still unconfirmed after X hours" for arbitrator misc refactoring and cleanup
This commit is contained in:
parent
ef4c55e32f
commit
15d2c24a82
@ -109,7 +109,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
|
||||
}
|
||||
|
||||
protected final void verifyTakerDepositConfirmed(TradeInfo trade) {
|
||||
if (!trade.getIsDepositUnlocked()) {
|
||||
if (!trade.getIsDepositsUnlocked()) {
|
||||
fail(format("INVALID_PHASE for trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.",
|
||||
trade.getShortId(),
|
||||
trade.getState(),
|
||||
@ -182,9 +182,9 @@ public class AbstractTradeTest extends AbstractOfferTest {
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase());
|
||||
|
||||
if (!isLongRunningTest)
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositsPublished());
|
||||
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositUnlocked());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositsUnlocked());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentStartedMessageSent, trade.getIsPaymentSent());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished());
|
||||
|
@ -231,7 +231,7 @@ public class BotClient {
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
|
||||
return grpcClient.getTrade(tradeId).getIsDepositUnlocked();
|
||||
return grpcClient.getTrade(tradeId).getIsDepositsUnlocked();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -301,10 +301,10 @@ public abstract class BotProtocol {
|
||||
}
|
||||
|
||||
private final Predicate<TradeInfo> isDepositFeeTxStepComplete = (trade) -> {
|
||||
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositPublished()) {
|
||||
if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_PUBLISHED) && trade.getIsDepositsPublished()) {
|
||||
log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId());
|
||||
return true;
|
||||
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositUnlocked()) {
|
||||
} else if (currentProtocolStep.equals(WAIT_FOR_TAKER_DEPOSIT_TX_CONFIRMED) && trade.getIsDepositsUnlocked()) {
|
||||
log.info("Taker deposit fee tx {} has been confirmed.", trade.getTakerDepositTxId());
|
||||
return true;
|
||||
} else {
|
||||
|
@ -65,8 +65,8 @@ class TradeDetailTableBuilder extends AbstractTradeListBuilder {
|
||||
colAmount.addRow(toTradeAmount.apply(trade));
|
||||
colMinerTxFee.addRow(toMyMinerTxFee.apply(trade));
|
||||
colBisqTradeFee.addRow(toMyMakerOrTakerFee.apply(trade));
|
||||
colIsDepositPublished.addRow(trade.getIsDepositPublished());
|
||||
colIsDepositConfirmed.addRow(trade.getIsDepositUnlocked());
|
||||
colIsDepositPublished.addRow(trade.getIsDepositsPublished());
|
||||
colIsDepositConfirmed.addRow(trade.getIsDepositsUnlocked());
|
||||
colTradeCost.addRow(toTradeVolumeAsString.apply(trade));
|
||||
colIsPaymentStartedMessageSent.addRow(trade.getIsPaymentSent());
|
||||
colIsPaymentReceivedMessageSent.addRow(trade.getIsPaymentReceived());
|
||||
|
@ -164,7 +164,7 @@ public class CoreAccountService {
|
||||
|
||||
public void deleteAccount(Runnable onShutdown) {
|
||||
try {
|
||||
keyRing.lockKeys();
|
||||
if (isAccountOpen()) closeAccount();
|
||||
synchronized (listeners) {
|
||||
for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown);
|
||||
}
|
||||
|
@ -254,9 +254,12 @@ public final class CoreMoneroConnectionsService {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- APP METHODS ------------------------------
|
||||
public void verifyConnection() {
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero node");
|
||||
if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced");
|
||||
}
|
||||
|
||||
public boolean isChainHeightSyncedWithinTolerance() {
|
||||
public boolean isSyncedWithinTolerance() {
|
||||
if (daemon == null) return false;
|
||||
Long targetHeight = lastInfo.getTargetHeight(); // the last time the node thought it was behind the network and was in active sync mode to catch up
|
||||
if (targetHeight == 0) return true; // monero-daemon-rpc sync_info's target_height returns 0 when node is fully synced
|
||||
@ -268,6 +271,8 @@ public final class CoreMoneroConnectionsService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------- APP METHODS ------------------------------
|
||||
|
||||
public ReadOnlyIntegerProperty numPeersProperty() {
|
||||
return numPeers;
|
||||
}
|
||||
|
@ -82,9 +82,9 @@ public class TradeInfo implements Payload {
|
||||
private final String periodState;
|
||||
private final String payoutState;
|
||||
private final String disputeState;
|
||||
private final boolean isDepositPublished;
|
||||
private final boolean isDepositConfirmed;
|
||||
private final boolean isDepositUnlocked;
|
||||
private final boolean isDepositsPublished;
|
||||
private final boolean isDepositsConfirmed;
|
||||
private final boolean isDepositsUnlocked;
|
||||
private final boolean isPaymentSent;
|
||||
private final boolean isPaymentReceived;
|
||||
private final boolean isPayoutPublished;
|
||||
@ -117,9 +117,9 @@ public class TradeInfo implements Payload {
|
||||
this.periodState = builder.getPeriodState();
|
||||
this.payoutState = builder.getPayoutState();
|
||||
this.disputeState = builder.getDisputeState();
|
||||
this.isDepositPublished = builder.isDepositPublished();
|
||||
this.isDepositConfirmed = builder.isDepositConfirmed();
|
||||
this.isDepositUnlocked = builder.isDepositUnlocked();
|
||||
this.isDepositsPublished = builder.isDepositsPublished();
|
||||
this.isDepositsConfirmed = builder.isDepositsConfirmed();
|
||||
this.isDepositsUnlocked = builder.isDepositsUnlocked();
|
||||
this.isPaymentSent = builder.isPaymentSent();
|
||||
this.isPaymentReceived = builder.isPaymentReceived();
|
||||
this.isPayoutPublished = builder.isPayoutPublished();
|
||||
@ -175,9 +175,9 @@ public class TradeInfo implements Payload {
|
||||
.withPeriodState(trade.getPeriodState().name())
|
||||
.withPayoutState(trade.getPayoutState().name())
|
||||
.withDisputeState(trade.getDisputeState().name())
|
||||
.withIsDepositPublished(trade.isDepositPublished())
|
||||
.withIsDepositConfirmed(trade.isDepositConfirmed())
|
||||
.withIsDepositUnlocked(trade.isDepositUnlocked())
|
||||
.withIsDepositsPublished(trade.isDepositsPublished())
|
||||
.withIsDepositsConfirmed(trade.isDepositsConfirmed())
|
||||
.withIsDepositsUnlocked(trade.isDepositsUnlocked())
|
||||
.withIsPaymentSent(trade.isPaymentSent())
|
||||
.withIsPaymentReceived(trade.isPaymentReceived())
|
||||
.withIsPayoutPublished(trade.isPayoutPublished())
|
||||
@ -219,9 +219,9 @@ public class TradeInfo implements Payload {
|
||||
.setPeriodState(periodState)
|
||||
.setPayoutState(payoutState)
|
||||
.setDisputeState(disputeState)
|
||||
.setIsDepositPublished(isDepositPublished)
|
||||
.setIsDepositConfirmed(isDepositConfirmed)
|
||||
.setIsDepositUnlocked(isDepositUnlocked)
|
||||
.setIsDepositsPublished(isDepositsPublished)
|
||||
.setIsDepositsConfirmed(isDepositsConfirmed)
|
||||
.setIsDepositsUnlocked(isDepositsUnlocked)
|
||||
.setIsPaymentSent(isPaymentSent)
|
||||
.setIsPaymentReceived(isPaymentReceived)
|
||||
.setIsCompleted(isCompleted)
|
||||
@ -257,9 +257,9 @@ public class TradeInfo implements Payload {
|
||||
.withPhase(proto.getPhase())
|
||||
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
|
||||
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
|
||||
.withIsDepositPublished(proto.getIsDepositPublished())
|
||||
.withIsDepositConfirmed(proto.getIsDepositConfirmed())
|
||||
.withIsDepositUnlocked(proto.getIsDepositUnlocked())
|
||||
.withIsDepositsPublished(proto.getIsDepositsPublished())
|
||||
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
|
||||
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
|
||||
.withIsPaymentSent(proto.getIsPaymentSent())
|
||||
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
||||
.withIsCompleted(proto.getIsCompleted())
|
||||
@ -294,9 +294,9 @@ public class TradeInfo implements Payload {
|
||||
", periodState='" + periodState + '\'' + "\n" +
|
||||
", payoutState='" + payoutState + '\'' + "\n" +
|
||||
", disputeState='" + disputeState + '\'' + "\n" +
|
||||
", isDepositPublished=" + isDepositPublished + "\n" +
|
||||
", isDepositConfirmed=" + isDepositConfirmed + "\n" +
|
||||
", isDepositUnlocked=" + isDepositUnlocked + "\n" +
|
||||
", isDepositsPublished=" + isDepositsPublished + "\n" +
|
||||
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
|
||||
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
|
||||
", isPaymentSent=" + isPaymentSent + "\n" +
|
||||
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||
|
@ -55,9 +55,9 @@ public final class TradeInfoV1Builder {
|
||||
private String periodState;
|
||||
private String payoutState;
|
||||
private String disputeState;
|
||||
private boolean isDepositPublished;
|
||||
private boolean isDepositConfirmed;
|
||||
private boolean isDepositUnlocked;
|
||||
private boolean isDepositsPublished;
|
||||
private boolean isDepositsConfirmed;
|
||||
private boolean isDepositsUnlocked;
|
||||
private boolean isPaymentSent;
|
||||
private boolean isPaymentReceived;
|
||||
private boolean isPayoutPublished;
|
||||
@ -183,18 +183,18 @@ public final class TradeInfoV1Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsDepositPublished(boolean isDepositPublished) {
|
||||
this.isDepositPublished = isDepositPublished;
|
||||
public TradeInfoV1Builder withIsDepositsPublished(boolean isDepositsPublished) {
|
||||
this.isDepositsPublished = isDepositsPublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsDepositConfirmed(boolean isDepositConfirmed) {
|
||||
this.isDepositConfirmed = isDepositConfirmed;
|
||||
public TradeInfoV1Builder withIsDepositsConfirmed(boolean isDepositsConfirmed) {
|
||||
this.isDepositsConfirmed = isDepositsConfirmed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) {
|
||||
this.isDepositUnlocked = isDepositUnlocked;
|
||||
public TradeInfoV1Builder withIsDepositsUnlocked(boolean isDepositsUnlocked) {
|
||||
this.isDepositsUnlocked = isDepositsUnlocked;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -500,7 +500,7 @@ public class HavenoSetup {
|
||||
revolutAccountsUpdateHandler,
|
||||
amazonGiftCardAccountsUpdateHandler);
|
||||
|
||||
if (walletsSetup.downloadPercentageProperty().get() == 1) {
|
||||
if (walletsSetup.downloadPercentageProperty().get() == 1) { // TODO: update for XMR
|
||||
checkForLockedUpFunds();
|
||||
checkForInvalidMakerFeeTxs();
|
||||
}
|
||||
|
@ -91,11 +91,15 @@ public class Balances {
|
||||
|
||||
private void updatedBalances() {
|
||||
if (!xmrWalletService.isWalletReady()) return;
|
||||
updateAvailableBalance();
|
||||
updatePendingBalance();
|
||||
updateReservedOfferBalance();
|
||||
updateReservedTradeBalance();
|
||||
updateReservedBalance();
|
||||
try {
|
||||
updateAvailableBalance();
|
||||
updatePendingBalance();
|
||||
updateReservedOfferBalance();
|
||||
updateReservedTradeBalance();
|
||||
updateReservedBalance();
|
||||
} catch (Exception e) {
|
||||
if (xmrWalletService.isWalletReady()) throw e; // ignore exception if wallet isn't ready
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (woodser): converting to long should generally be avoided since can lose precision, but in practice these amounts are below max value
|
||||
|
@ -491,6 +491,7 @@ public class XmrWalletService {
|
||||
synchronized (txCache) {
|
||||
|
||||
// fetch txs
|
||||
if (getDaemon() == null) connectionsService.verifyConnection(); // will throw
|
||||
List<MoneroTx> txs = getDaemon().getTxs(txHashes, true);
|
||||
|
||||
// store to cache
|
||||
@ -549,6 +550,7 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
private void maybeInitMainWallet() {
|
||||
if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
|
||||
|
||||
// open or create wallet
|
||||
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
||||
@ -560,14 +562,9 @@ public class XmrWalletService {
|
||||
|
||||
// wallet is not initialized until connected to a daemon
|
||||
if (wallet != null) {
|
||||
try {
|
||||
wallet.sync(); // blocking
|
||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); // start syncing wallet in background
|
||||
connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both
|
||||
saveMainWallet(false); // skip backup on open
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// sync wallet which updates app startup state
|
||||
trySyncMainWallet();
|
||||
|
||||
if (connectionsService.getDaemon() == null) System.out.println("Daemon: null");
|
||||
else {
|
||||
@ -671,12 +668,25 @@ public class XmrWalletService {
|
||||
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
||||
}
|
||||
|
||||
private void trySyncMainWallet() {
|
||||
try {
|
||||
log.info("Syncing main wallet");
|
||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); // start syncing wallet in background
|
||||
wallet.sync(); // blocking
|
||||
connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both
|
||||
log.info("Done syncing main wallet");
|
||||
saveMainWallet(false); // skip backup on open
|
||||
} catch (Exception e) {
|
||||
log.warn("Error syncing main wallet: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||
log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
||||
if (wallet == null) maybeInitMainWallet();
|
||||
if (wallet != null) {
|
||||
else {
|
||||
wallet.setDaemonConnection(connection);
|
||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
||||
if (connection != null) new Thread(() -> trySyncMainWallet()).start();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1008,7 +1008,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
|
||||
// Don't allow trade start if Monero node is not fully synced
|
||||
if (!connectionService.isChainHeightSyncedWithinTolerance()) {
|
||||
if (!connectionService.isSyncedWithinTolerance()) {
|
||||
errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced.";
|
||||
log.info(errorMessage);
|
||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||
|
@ -50,6 +50,9 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// verify monero connection
|
||||
model.getXmrWalletService().getConnectionsService().verifyConnection();
|
||||
|
||||
// create reserve tx
|
||||
BigInteger makerFee = HavenoUtils.coinToAtomicUnits(offer.getMakerFee());
|
||||
BigInteger sendAmount = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? Coin.ZERO : offer.getAmount());
|
||||
|
@ -20,8 +20,11 @@ package bisq.core.support;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.protocol.TradeProtocol;
|
||||
import bisq.core.trade.protocol.TradeProtocol.MailboxMessageComparator;
|
||||
import bisq.network.p2p.AckMessage;
|
||||
@ -51,6 +54,7 @@ import javax.annotation.Nullable;
|
||||
@Slf4j
|
||||
public abstract class SupportManager {
|
||||
protected final P2PService p2PService;
|
||||
protected final TradeManager tradeManager;
|
||||
protected final CoreMoneroConnectionsService connectionService;
|
||||
protected final CoreNotificationService notificationService;
|
||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||
@ -65,11 +69,15 @@ public abstract class SupportManager {
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, CoreNotificationService notificationService) {
|
||||
public SupportManager(P2PService p2PService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager) {
|
||||
this.p2PService = p2PService;
|
||||
this.connectionService = connectionService;
|
||||
this.mailboxMessageService = p2PService.getMailboxMessageService();
|
||||
this.notificationService = notificationService;
|
||||
this.tradeManager = tradeManager;
|
||||
|
||||
// We get first the message handler called then the onBootstrapped
|
||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
@ -181,6 +189,18 @@ public abstract class SupportManager {
|
||||
if (ackMessage.isSuccess()) {
|
||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
|
||||
// dispute is opened by ack on chat message
|
||||
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
||||
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
|
||||
for (Dispute dispute : trade.getDisputes()) {
|
||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
||||
|
@ -47,7 +47,6 @@ import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.SendMailboxMessageListener;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
@ -94,7 +93,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager {
|
||||
protected final TradeWalletService tradeWalletService;
|
||||
protected final XmrWalletService xmrWalletService;
|
||||
protected final TradeManager tradeManager;
|
||||
protected final ClosedTradableManager closedTradableManager;
|
||||
protected final OpenOfferManager openOfferManager;
|
||||
protected final KeyRing keyRing;
|
||||
@ -122,11 +120,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
DisputeListService<T> disputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, connectionService, notificationService);
|
||||
super(p2PService, connectionService, notificationService, tradeManager);
|
||||
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
this.tradeManager = tradeManager;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.keyRing = keyRing;
|
||||
@ -234,7 +231,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
}
|
||||
|
||||
protected T getDisputeList() {
|
||||
return disputeListService.getDisputeList();
|
||||
synchronized(disputeListService.getDisputeList()) {
|
||||
return disputeListService.getDisputeList();
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getDisputedTradeIds() {
|
||||
@ -367,7 +366,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType(),
|
||||
updatedMultisigHex,
|
||||
trade.getBuyer().getPaymentSentMessage());
|
||||
trade.getProcessModel().getPaymentSentMessage());
|
||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
@ -388,7 +387,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
requestPersistence();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
@ -404,7 +403,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
requestPersistence();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
@ -441,86 +440,97 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
Dispute dispute = message.getDispute();
|
||||
log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||
|
||||
// intialize
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return;
|
||||
}
|
||||
dispute.setSupportType(message.getSupportType());
|
||||
dispute.setState(Dispute.State.NEW); // TODO: unused, remove?
|
||||
Contract contract = dispute.getContract();
|
||||
|
||||
// validate dispute
|
||||
try {
|
||||
TradeDataValidation.validatePaymentAccountPayload(dispute);
|
||||
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
||||
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
||||
TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config);
|
||||
TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config);
|
||||
} catch (TradeDataValidation.AddressException |
|
||||
TradeDataValidation.NodeAddressException |
|
||||
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
||||
log.error(e.toString());
|
||||
validationExceptions.add(e);
|
||||
}
|
||||
|
||||
// get trade
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
if (trade == null) {
|
||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||
return;
|
||||
}
|
||||
|
||||
// get sender
|
||||
PubKeyRing senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||
TradingPeer sender = trade.getTradingPeer(senderPubKeyRing);
|
||||
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||
|
||||
// message to trader is expected from arbitrator
|
||||
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
||||
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
||||
}
|
||||
|
||||
// arbitrator verifies signature of payment sent message if given
|
||||
if (trade.isArbitrator() && message.getPaymentSentMessage() != null) {
|
||||
HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
||||
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
||||
trade.setStateIfProgress(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||
}
|
||||
|
||||
// update multisig hex
|
||||
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
|
||||
// update peer node address
|
||||
// TODO: tests can reuse the same addresses so nullify equal peer
|
||||
sender.setNodeAddress(message.getSenderNodeAddress());
|
||||
|
||||
// add chat message with price info
|
||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||
|
||||
// add dispute
|
||||
Trade trade = null;
|
||||
String errorMessage = null;
|
||||
synchronized (disputeList) {
|
||||
if (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
disputeList.add(dispute);
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
||||
|
||||
// send dispute opened message to peer if arbitrator
|
||||
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
||||
tradeManager.requestPersistence();
|
||||
errorMessage = null;
|
||||
} else {
|
||||
// valid case if both have opened a dispute and agent was not online
|
||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||
dispute.getTradeId());
|
||||
}
|
||||
} else {
|
||||
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
PubKeyRing senderPubKeyRing = null;
|
||||
try {
|
||||
|
||||
// intialize
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return;
|
||||
}
|
||||
dispute.setSupportType(message.getSupportType());
|
||||
dispute.setState(Dispute.State.NEW);
|
||||
Contract contract = dispute.getContract();
|
||||
|
||||
// validate dispute
|
||||
try {
|
||||
TradeDataValidation.validatePaymentAccountPayload(dispute);
|
||||
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
||||
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
||||
TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config);
|
||||
TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config);
|
||||
} catch (TradeDataValidation.AddressException |
|
||||
TradeDataValidation.NodeAddressException |
|
||||
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
||||
validationExceptions.add(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// get trade
|
||||
trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
if (trade == null) {
|
||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||
return;
|
||||
}
|
||||
|
||||
// get sender
|
||||
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||
TradingPeer sender = trade.getTradingPeer(senderPubKeyRing);
|
||||
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||
|
||||
// message to trader is expected from arbitrator
|
||||
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
||||
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
||||
}
|
||||
|
||||
// arbitrator verifies signature of payment sent message if given
|
||||
if (trade.isArbitrator() && message.getPaymentSentMessage() != null) {
|
||||
HavenoUtils.verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
||||
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
||||
trade.setStateIfProgress(sender == trade.getBuyer() ? Trade.State.BUYER_SENT_PAYMENT_SENT_MSG : Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||
}
|
||||
|
||||
// update multisig hex
|
||||
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
|
||||
// update peer node address
|
||||
// TODO: tests can reuse the same addresses so nullify equal peer
|
||||
sender.setNodeAddress(message.getSenderNodeAddress());
|
||||
|
||||
// add chat message with price info
|
||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||
|
||||
// add dispute
|
||||
synchronized (disputeList) {
|
||||
if (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
disputeList.add(dispute);
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
||||
|
||||
// send dispute opened message to peer if arbitrator
|
||||
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
||||
tradeManager.requestPersistence();
|
||||
errorMessage = null;
|
||||
} else {
|
||||
// valid case if both have opened a dispute and agent was not online
|
||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||
dispute.getTradeId());
|
||||
}
|
||||
|
||||
// add chat message with mediation info if applicable
|
||||
addMediationResultMessage(dispute);
|
||||
} else {
|
||||
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errorMessage = e.getMessage();
|
||||
log.warn(errorMessage);
|
||||
if (trade != null) trade.setErrorMessage(errorMessage);
|
||||
}
|
||||
|
||||
// use chat message instead of open dispute message for the ack
|
||||
@ -530,9 +540,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
||||
}
|
||||
|
||||
// add chat message with mediation info if applicable // TODO: not applicable in haveno
|
||||
addMediationResultMessage(dispute);
|
||||
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
@ -635,7 +642,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType(),
|
||||
updatedMultisigHex,
|
||||
trade.getSelf().getPaymentSentMessage());
|
||||
trade.getProcessModel().getPaymentSentMessage());
|
||||
|
||||
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
|
@ -71,7 +71,7 @@ public class DisputeSummaryVerification {
|
||||
String fullAddress = textToSign.split("\n")[1].split(": ")[1];
|
||||
NodeAddress nodeAddress = new NodeAddress(fullAddress);
|
||||
DisputeAgent disputeAgent = arbitratorMediator.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
|
||||
checkNotNull(disputeAgent);
|
||||
checkNotNull(disputeAgent, "Dispute agent is null");
|
||||
PublicKey pubKey = disputeAgent.getPubKeyRing().getSignaturePubKey();
|
||||
|
||||
String sigString = parts[1].split(SEPARATOR2)[0];
|
||||
|
@ -56,8 +56,12 @@ import com.google.inject.Singleton;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@ -77,6 +81,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
private final ArbitratorManager arbitratorManager;
|
||||
|
||||
private Map<String, Integer> reprocessDisputeClosedMessageCounts = new HashMap<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -117,15 +123,17 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
log.info("Received {} from {} with tradeId {} and uid {}",
|
||||
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
|
||||
|
||||
if (message instanceof DisputeOpenedMessage) {
|
||||
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
handleChatMessage((ChatMessage) message);
|
||||
} else if (message instanceof DisputeClosedMessage) {
|
||||
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
new Thread(() -> {
|
||||
if (message instanceof DisputeOpenedMessage) {
|
||||
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
handleChatMessage((ChatMessage) message);
|
||||
} else if (message instanceof DisputeClosedMessage) {
|
||||
handleDisputeClosedMessage((DisputeClosedMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,121 +181,166 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
// received by both peers when arbitrator closes disputes
|
||||
@Override
|
||||
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
|
||||
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||
String tradeId = disputeResult.getTradeId();
|
||||
|
||||
// get trade
|
||||
Trade trade = tradeManager.getTrade(tradeId);
|
||||
if (trade == null) {
|
||||
log.warn("Dispute trade {} does not exist", tradeId);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId());
|
||||
|
||||
// verify arbitrator signature
|
||||
String summaryText = chatMessage.getMessage();
|
||||
DisputeSummaryVerification.verifySignature(summaryText, arbitratorManager);
|
||||
|
||||
// get dispute
|
||||
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
||||
String uid = disputeClosedMessage.getUid();
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("We got a dispute closed msg but we don't have a matching dispute. " +
|
||||
"That might happen when we get the DisputeClosedMessage before the dispute was created. " +
|
||||
"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);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " +
|
||||
"That should never happen. TradeId = " + tradeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Dispute dispute = disputeOptional.get();
|
||||
|
||||
// verify that arbitrator does not get DisputeClosedMessage
|
||||
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
||||
log.error("Arbitrator received disputeResultMessage. That should never happen.");
|
||||
return;
|
||||
}
|
||||
|
||||
// set dispute state
|
||||
cleanupRetryMap(uid);
|
||||
if (!dispute.getChatMessages().contains(chatMessage)) {
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
} else {
|
||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||
}
|
||||
dispute.setIsClosed();
|
||||
if (dispute.disputeResultProperty().get() != null) {
|
||||
log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId);
|
||||
}
|
||||
dispute.setDisputeResult(disputeResult);
|
||||
|
||||
// import multisig hex
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||
|
||||
// sync and save wallet
|
||||
trade.syncWallet();
|
||||
trade.saveWallet();
|
||||
|
||||
// run off main thread
|
||||
new Thread(() -> {
|
||||
String errorMessage = null;
|
||||
boolean success = true;
|
||||
|
||||
// attempt to sign and publish dispute payout tx if given and not already published
|
||||
if (disputeClosedMessage.getUnsignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
|
||||
|
||||
// wait to sign and publish payout tx if defer flag set
|
||||
if (disputeClosedMessage.isDeferPublishPayout()) {
|
||||
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||
trade.syncWallet();
|
||||
}
|
||||
|
||||
// sign and publish dispute payout tx if peer still has not published
|
||||
if (!trade.isPayoutPublished()) {
|
||||
try {
|
||||
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
signAndPublishDisputePayoutTx(trade, disputeClosedMessage.getUnsignedPayoutTxHex());
|
||||
} catch (Exception e) {
|
||||
|
||||
// check if payout published again
|
||||
trade.syncWallet();
|
||||
if (trade.isPayoutPublished()) {
|
||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
} else {
|
||||
e.printStackTrace();
|
||||
errorMessage = "Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId;
|
||||
log.warn(errorMessage);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
}
|
||||
} else {
|
||||
if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getName(), trade.getId());
|
||||
}
|
||||
|
||||
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
|
||||
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
|
||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
||||
requestPersistence();
|
||||
}).start();
|
||||
handleDisputeClosedMessage(disputeClosedMessage, true);
|
||||
}
|
||||
|
||||
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade, String payoutTxHex) {
|
||||
private void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage, boolean reprocessOnError) {
|
||||
|
||||
// get dispute's trade
|
||||
final Trade trade = tradeManager.getTrade(disputeClosedMessage.getTradeId());
|
||||
if (trade == null) {
|
||||
log.warn("Dispute trade {} does not exist", disputeClosedMessage.getTradeId());
|
||||
return;
|
||||
}
|
||||
|
||||
// try to process dispute closed message
|
||||
ChatMessage chatMessage = null;
|
||||
Dispute dispute = null;
|
||||
synchronized (trade) {
|
||||
try {
|
||||
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
||||
chatMessage = disputeResult.getChatMessage();
|
||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||
String tradeId = disputeResult.getTradeId();
|
||||
|
||||
log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId());
|
||||
|
||||
// verify arbitrator signature
|
||||
String summaryText = chatMessage.getMessage();
|
||||
DisputeSummaryVerification.verifySignature(summaryText, arbitratorManager);
|
||||
|
||||
// save dispute closed message for reprocessing
|
||||
trade.getProcessModel().setDisputeClosedMessage(disputeClosedMessage);
|
||||
requestPersistence();
|
||||
|
||||
// get dispute
|
||||
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
||||
String uid = disputeClosedMessage.getUid();
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("We got a dispute closed msg but we don't have a matching dispute. " +
|
||||
"That might happen when we get the DisputeClosedMessage before the dispute was created. " +
|
||||
"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);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " +
|
||||
"That should never happen. TradeId = " + tradeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
dispute = disputeOptional.get();
|
||||
|
||||
// verify that arbitrator does not get DisputeClosedMessage
|
||||
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
||||
log.error("Arbitrator received disputeResultMessage. That should never happen.");
|
||||
trade.getProcessModel().setDisputeClosedMessage(null); // don't reprocess
|
||||
return;
|
||||
}
|
||||
|
||||
// set dispute state
|
||||
cleanupRetryMap(uid);
|
||||
if (!dispute.getChatMessages().contains(chatMessage)) {
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
} else {
|
||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||
}
|
||||
dispute.setIsClosed();
|
||||
if (dispute.disputeResultProperty().get() != null) {
|
||||
log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId);
|
||||
}
|
||||
dispute.setDisputeResult(disputeResult);
|
||||
|
||||
// attempt to sign and publish dispute payout tx if given and not already published
|
||||
if (disputeClosedMessage.getUnsignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
|
||||
|
||||
// check wallet connection
|
||||
trade.checkWalletConnection();
|
||||
|
||||
// import multisig hex
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||
|
||||
// sync and save wallet
|
||||
trade.syncWallet();
|
||||
trade.saveWallet();
|
||||
|
||||
// wait to sign and publish payout tx if defer flag set
|
||||
if (disputeClosedMessage.isDeferPublishPayout()) {
|
||||
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||
}
|
||||
|
||||
// sign and publish dispute payout tx if peer still has not published
|
||||
if (!trade.isPayoutPublished()) {
|
||||
try {
|
||||
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
signAndPublishDisputePayoutTx(trade);
|
||||
} catch (Exception e) {
|
||||
|
||||
// check if payout published again
|
||||
trade.syncWallet();
|
||||
if (trade.isPayoutPublished()) {
|
||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
} else {
|
||||
throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
}
|
||||
} else {
|
||||
if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getName(), trade.getId());
|
||||
}
|
||||
|
||||
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
|
||||
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
|
||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||
requestPersistence();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error processing dispute closed message: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
requestPersistence();
|
||||
|
||||
// nack bad message and do not reprocess
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
trade.getProcessModel().setPaymentReceivedMessage(null); // message is processed
|
||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
|
||||
requestPersistence();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// reprocess on error
|
||||
if (trade.getProcessModel().getDisputeClosedMessage() != null) {
|
||||
if (!reprocessDisputeClosedMessageCounts.containsKey(trade.getId())) reprocessDisputeClosedMessageCounts.put(trade.getId(), 0);
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessDisputeClosedMessageCounts.put(trade.getId(), reprocessDisputeClosedMessageCounts.get(trade.getId()) + 1); // increment reprocess count
|
||||
maybeReprocessDisputeClosedMessage(trade, reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessDisputeClosedMessageCounts.get(trade.getId())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
|
||||
synchronized (trade) {
|
||||
|
||||
// skip if no need to reprocess
|
||||
if (trade.isArbitrator() || trade.getProcessModel().getDisputeClosedMessage() == null || trade.getProcessModel().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn("Reprocessing dispute closed message for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
new Thread(() -> handleDisputeClosedMessage(trade.getProcessModel().getDisputeClosedMessage(), reprocessOnError)).start();
|
||||
}
|
||||
}
|
||||
|
||||
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade) {
|
||||
|
||||
// gather trade info
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
@ -296,6 +349,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
Dispute dispute = disputeOptional.get();
|
||||
Contract contract = dispute.getContract();
|
||||
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
||||
String unsignedPayoutTxHex = trade.getProcessModel().getDisputeClosedMessage().getUnsignedPayoutTxHex();
|
||||
|
||||
// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
|
||||
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
|
||||
@ -303,9 +357,9 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
||||
|
||||
// parse arbitrator-signed payout tx
|
||||
MoneroTxSet signedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||
if (signedTxSet.getTxs() == null || signedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
||||
MoneroTxWallet arbitratorSignedPayoutTx = signedTxSet.getTxs().get(0);
|
||||
MoneroTxSet disputeTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(unsignedPayoutTxHex));
|
||||
if (disputeTxSet.getTxs() == null || disputeTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
||||
MoneroTxWallet arbitratorSignedPayoutTx = disputeTxSet.getTxs().get(0);
|
||||
|
||||
// verify payout tx has 1 or 2 destinations
|
||||
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
||||
@ -353,36 +407,55 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
||||
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
||||
|
||||
// sign arbitrator-signed payout tx
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||
signedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||
// check wallet's daemon connection
|
||||
trade.checkWalletConnection();
|
||||
|
||||
// verify mining fee is within tolerance by recreating payout tx
|
||||
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||
MoneroTxWallet feeEstimateTx = null;
|
||||
try {
|
||||
feeEstimateTx = createDisputePayoutTx(trade, dispute, disputeResult, true);
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not recreate dispute payout tx to verify fee: " + e.getMessage());
|
||||
// determine if we already signed dispute payout tx
|
||||
// TODO: better way, such as by saving signed dispute payout tx hex in designated field instead of shared payoutTxHex field?
|
||||
Set<String> nonSignedDisputePayoutTxHexes = new HashSet<String>();
|
||||
if (trade.getProcessModel().getPaymentSentMessage() != null) nonSignedDisputePayoutTxHexes.add(trade.getProcessModel().getPaymentSentMessage().getPayoutTxHex());
|
||||
if (trade.getProcessModel().getPaymentReceivedMessage() != null) {
|
||||
nonSignedDisputePayoutTxHexes.add(trade.getProcessModel().getPaymentReceivedMessage().getUnsignedPayoutTxHex());
|
||||
nonSignedDisputePayoutTxHexes.add(trade.getProcessModel().getPaymentReceivedMessage().getSignedPayoutTxHex());
|
||||
}
|
||||
if (feeEstimateTx != null) {
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
|
||||
boolean signed = trade.getPayoutTxHex() != null && !nonSignedDisputePayoutTxHexes.contains(trade.getPayoutTxHex());
|
||||
|
||||
// sign arbitrator-signed payout tx
|
||||
if (!signed) {
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||
trade.setPayoutTxHex(signedMultisigTxHex);
|
||||
requestPersistence();
|
||||
|
||||
// verify mining fee is within tolerance by recreating payout tx
|
||||
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||
MoneroTxWallet feeEstimateTx = null;
|
||||
try {
|
||||
feeEstimateTx = createDisputePayoutTx(trade, dispute, disputeResult, true);
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not recreate dispute payout tx to verify fee: " + e.getMessage());
|
||||
}
|
||||
if (feeEstimateTx != null) {
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
|
||||
}
|
||||
} else {
|
||||
disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex());
|
||||
}
|
||||
|
||||
// submit fully signed payout tx to the network
|
||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
|
||||
signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
||||
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||
|
||||
// update state
|
||||
trade.setPayoutTx(signedTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||
trade.setPayoutTxId(signedTxSet.getTxs().get(0).getHash());
|
||||
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||
trade.setPayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
||||
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
||||
dispute.setDisputePayoutTxId(signedTxSet.getTxs().get(0).getHash());
|
||||
return signedTxSet;
|
||||
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
||||
return disputeTxSet;
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class TraderChatManager extends SupportManager {
|
||||
private final TradeManager tradeManager;
|
||||
private final PubKeyRingProvider pubKeyRingProvider;
|
||||
|
||||
|
||||
@ -61,8 +60,7 @@ public class TraderChatManager extends SupportManager {
|
||||
CoreNotificationService notificationService,
|
||||
TradeManager tradeManager,
|
||||
PubKeyRingProvider pubKeyRingProvider) {
|
||||
super(p2PService, connectionService, notificationService);
|
||||
this.tradeManager = tradeManager;
|
||||
super(p2PService, connectionService, notificationService, tradeManager);
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
}
|
||||
|
||||
|
@ -294,13 +294,13 @@ public class HavenoUtils {
|
||||
// verify signature
|
||||
String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
||||
try {
|
||||
if (!Sig.verify(trade.getBuyer().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage);
|
||||
if (!Sig.verify(trade.getBuyer().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new IllegalArgumentException(errMessage);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(errMessage);
|
||||
throw new IllegalArgumentException(errMessage);
|
||||
}
|
||||
|
||||
// verify trade id
|
||||
if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
|
||||
if (!trade.getId().equals(message.getTradeId())) throw new IllegalArgumentException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -325,13 +325,13 @@ public class HavenoUtils {
|
||||
// verify signature
|
||||
String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
||||
try {
|
||||
if (!Sig.verify(trade.getSeller().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new RuntimeException(errMessage);
|
||||
if (!Sig.verify(trade.getSeller().getPubKeyRing().getSignaturePubKey(), unsignedMessageAsJson.getBytes(Charsets.UTF_8), signature)) throw new IllegalArgumentException(errMessage);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(errMessage);
|
||||
throw new IllegalArgumentException(errMessage);
|
||||
}
|
||||
|
||||
// verify trade id
|
||||
if (!trade.getId().equals(message.getTradeId())) throw new RuntimeException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
|
||||
if (!trade.getId().equals(message.getTradeId())) throw new IllegalArgumentException("The " + message.getClass().getSimpleName() + " has the wrong trade id, expected " + trade.getId() + " but was " + message.getTradeId());
|
||||
|
||||
// verify buyer signature of payment sent message
|
||||
verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package bisq.core.trade;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.model.XmrAddressEntry;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
@ -240,7 +241,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
|
||||
public enum DisputeState {
|
||||
NO_DISPUTE,
|
||||
DISPUTE_REQUESTED, // TODO: not currently used; can use by subscribing to chat message ack in DisputeManager
|
||||
DISPUTE_REQUESTED,
|
||||
DISPUTE_OPENED,
|
||||
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG,
|
||||
ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG,
|
||||
@ -281,6 +282,14 @@ public abstract class Trade implements Tradable, Model {
|
||||
return this.ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isRequested() {
|
||||
return ordinal() >= DisputeState.DISPUTE_REQUESTED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return this == DisputeState.DISPUTE_OPENED;
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return this == DisputeState.DISPUTE_CLOSED;
|
||||
}
|
||||
@ -404,6 +413,9 @@ public abstract class Trade implements Tradable, Model {
|
||||
@Setter
|
||||
private long lockTime;
|
||||
@Getter
|
||||
@Setter
|
||||
private long startTime; // added for haveno
|
||||
@Getter
|
||||
@Nullable
|
||||
private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT;
|
||||
transient final private ObjectProperty<RefundResultState> refundResultStateProperty = new SimpleObjectProperty<>(refundResultState);
|
||||
@ -444,8 +456,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
@Getter
|
||||
@Setter
|
||||
private String payoutTxKey;
|
||||
private Long startTime; // cache
|
||||
|
||||
private static final long MAX_REPROCESS_DELAY_SECONDS = 7200; // max delay to reprocess messages (once per 2 hours)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructors
|
||||
@ -588,7 +600,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
// handle trade state events
|
||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||
if (!isInitialized) return;
|
||||
if (isDepositPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
||||
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
||||
if (isCompleted()) {
|
||||
UserThread.execute(() -> {
|
||||
if (tradePhaseSubscription != null) {
|
||||
@ -648,6 +660,10 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
public void requestPersistence() {
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
public TradeProtocol getProtocol() {
|
||||
return processModel.getTradeManager().getTradeProtocol(this);
|
||||
}
|
||||
@ -664,6 +680,22 @@ public abstract class Trade implements Tradable, Model {
|
||||
return getArbitrator() == null ? null : getArbitrator().getNodeAddress();
|
||||
}
|
||||
|
||||
public void checkWalletConnection() {
|
||||
CoreMoneroConnectionsService connectionService = xmrWalletService.getConnectionsService();
|
||||
connectionService.checkConnection();
|
||||
connectionService.verifyConnection();
|
||||
if (!getWallet().isConnectedToDaemon()) throw new RuntimeException("Wallet is not connected to a Monero node");
|
||||
}
|
||||
|
||||
public boolean isWalletConnected() {
|
||||
try {
|
||||
checkWalletConnection();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a contract based on the current state.
|
||||
*
|
||||
@ -717,6 +749,9 @@ public abstract class Trade implements Tradable, Model {
|
||||
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
|
||||
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
|
||||
|
||||
// check connection to monero daemon
|
||||
checkWalletConnection();
|
||||
|
||||
// create transaction to get fee estimate
|
||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
@ -760,20 +795,19 @@ public abstract class Trade implements Tradable, Model {
|
||||
log.info("Verifying payout tx");
|
||||
|
||||
// gather relevant info
|
||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(getId());
|
||||
MoneroWallet wallet = getWallet();
|
||||
Contract contract = getContract();
|
||||
BigInteger sellerDepositAmount = multisigWallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
|
||||
BigInteger buyerDepositAmount = multisigWallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
|
||||
BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
|
||||
BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
|
||||
BigInteger tradeAmount = HavenoUtils.coinToAtomicUnits(getAmount());
|
||||
|
||||
// describe payout tx
|
||||
MoneroTxSet describedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad payout tx"); // TODO (woodser): test nack
|
||||
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
|
||||
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
|
||||
|
||||
// verify payout tx has exactly 2 destinations
|
||||
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Payout tx does not have exactly two destinations");
|
||||
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations");
|
||||
|
||||
// get buyer and seller destinations (order not preserved)
|
||||
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
|
||||
@ -781,32 +815,35 @@ public abstract class Trade implements Tradable, Model {
|
||||
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
|
||||
|
||||
// verify payout addresses
|
||||
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
|
||||
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
||||
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract");
|
||||
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract");
|
||||
|
||||
// verify change address is multisig's primary address
|
||||
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
||||
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address");
|
||||
|
||||
// verify sum of outputs = destination amounts + change amount
|
||||
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
||||
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount");
|
||||
|
||||
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
|
||||
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
|
||||
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
||||
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
|
||||
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
|
||||
|
||||
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
|
||||
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
||||
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
||||
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
||||
|
||||
// check wallet's daemon connection
|
||||
checkWalletConnection();
|
||||
|
||||
// handle tx signing
|
||||
if (sign) {
|
||||
|
||||
// sign tx
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
||||
payoutTxHex = result.getSignedMultisigTxHex();
|
||||
describedTxSet = multisigWallet.describeMultisigTxSet(payoutTxHex); // update described set
|
||||
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); // update described set
|
||||
payoutTx = describedTxSet.getTxs().get(0);
|
||||
|
||||
// verify fee is within tolerance by recreating payout tx
|
||||
@ -820,7 +857,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
if (feeEstimateTx != null) {
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||
}
|
||||
}
|
||||
@ -831,7 +868,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
|
||||
// submit payout tx
|
||||
if (publish) {
|
||||
multisigWallet.submitMultisigTxHex(payoutTxHex);
|
||||
//if (true) throw new RuntimeException("Let's pretend there's an error last second submitting tx to daemon, so we need to resubmit payout hex");
|
||||
wallet.submitMultisigTxHex(payoutTxHex);
|
||||
pollWallet();
|
||||
}
|
||||
}
|
||||
@ -926,14 +964,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
public void syncWallet() {
|
||||
if (getWallet() == null) {
|
||||
log.warn("Cannot sync multisig wallet because it doesn't exist for {}, {}", getClass().getSimpleName(), getId());
|
||||
return;
|
||||
}
|
||||
if (getWallet().getDaemonConnection() == null) {
|
||||
log.warn("Cannot sync multisig wallet because it's not connected to a Monero daemon for {}, {}", getClass().getSimpleName(), getId());
|
||||
return;
|
||||
}
|
||||
if (getWallet() == null) throw new RuntimeException("Cannot sync multisig wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
||||
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync multisig wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
|
||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
getWallet().sync();
|
||||
pollWallet();
|
||||
@ -941,6 +973,14 @@ public abstract class Trade implements Tradable, Model {
|
||||
updateWalletRefreshPeriod();
|
||||
}
|
||||
|
||||
private void trySyncWallet() {
|
||||
try {
|
||||
syncWallet();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error syncing wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void syncWalletNormallyForMs(long syncNormalDuration) {
|
||||
syncNormalStartTime = System.currentTimeMillis();
|
||||
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
||||
@ -957,7 +997,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
if (xmrWalletService.multisigWalletExists(getId())) {
|
||||
|
||||
// delete trade wallet unless funded
|
||||
if (isDepositPublished() && !isPayoutUnlocked()) {
|
||||
if (isDepositsPublished() && !isPayoutUnlocked()) {
|
||||
log.warn("Refusing to delete wallet for {} {} because it could be funded", getClass().getSimpleName(), getId());
|
||||
return;
|
||||
}
|
||||
@ -1258,36 +1298,37 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
private long getStartTime() {
|
||||
if (startTime != null) return startTime;
|
||||
long now = System.currentTimeMillis();
|
||||
if (isDepositConfirmed() && getTakeOfferDate() != null) {
|
||||
if (isDepositUnlocked()) {
|
||||
final long tradeTime = getTakeOfferDate().getTime();
|
||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||
|
||||
// if (depositTx.getConfidence().getDepthInBlocks() > 0) {
|
||||
// final long tradeTime = getTakeOfferDate().getTime();
|
||||
// // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime()
|
||||
// long blockTime = depositTx.getIncludedInBestChainAt() != null ? depositTx.getIncludedInBestChainAt().getTime() : depositTx.getUpdateTime().getTime();
|
||||
// If block date is in future (Date in Bitcoin blocks can be off by +/- 2 hours) we use our current date.
|
||||
// If block date is earlier than our trade date we use our trade date.
|
||||
if (blockTime > now)
|
||||
startTime = now;
|
||||
else
|
||||
startTime = Math.max(blockTime, tradeTime);
|
||||
|
||||
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
|
||||
new Date(startTime), new Date(tradeTime), new Date(blockTime));
|
||||
if (isDepositsConfirmed() && getTakeOfferDate() != null) {
|
||||
if (isDepositsUnlocked()) {
|
||||
if (startTime <= 0) setStartTimeFromUnlockedTxs(); // save to model
|
||||
return startTime;
|
||||
} else {
|
||||
log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", getMaker().getDepositTxHash(), getTaker().getDepositTxHash());
|
||||
startTime = now;
|
||||
return now;
|
||||
}
|
||||
} else {
|
||||
startTime = now;
|
||||
return now;
|
||||
}
|
||||
return startTime;
|
||||
}
|
||||
|
||||
private void setStartTimeFromUnlockedTxs() {
|
||||
long now = System.currentTimeMillis();
|
||||
final long tradeTime = getTakeOfferDate().getTime();
|
||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||
if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod");
|
||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
||||
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||
|
||||
// If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date.
|
||||
// If block date is earlier than our trade date we use our trade date.
|
||||
if (blockTime > now)
|
||||
startTime = now;
|
||||
else
|
||||
startTime = Math.max(blockTime, tradeTime);
|
||||
|
||||
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
|
||||
new Date(startTime), new Date(tradeTime), new Date(blockTime));
|
||||
}
|
||||
|
||||
public boolean hasFailed() {
|
||||
@ -1306,19 +1347,19 @@ public abstract class Trade implements Tradable, Model {
|
||||
return getState() == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED;
|
||||
}
|
||||
|
||||
public boolean isDepositPublished() {
|
||||
public boolean isDepositsPublished() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isFundsLockedIn() {
|
||||
return isDepositPublished() && !isPayoutPublished();
|
||||
return isDepositsPublished() && !isPayoutPublished();
|
||||
}
|
||||
|
||||
public boolean isDepositConfirmed() {
|
||||
public boolean isDepositsConfirmed() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isDepositUnlocked() {
|
||||
public boolean isDepositsUnlocked() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
||||
}
|
||||
|
||||
@ -1458,6 +1499,19 @@ public abstract class Trade implements Tradable, Model {
|
||||
processModel.getTaker().getDepositTxHash() == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration to delay reprocessing a message based on its reprocess count.
|
||||
*
|
||||
* @return the duration to delay in seconds
|
||||
*/
|
||||
public long getReprocessDelayInSeconds(int reprocessCount) {
|
||||
int retryCycles = 3; // reprocess on next refresh periods for first few attempts (app might auto switch to a good connection)
|
||||
if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs() / 1000;
|
||||
long delay = 60;
|
||||
for (int i = retryCycles; i < reprocessCount; i++) delay *= 2;
|
||||
return Math.min(MAX_REPROCESS_DELAY_SECONDS, delay);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
@ -1479,18 +1533,27 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||
if (getWallet() == null) return;
|
||||
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
|
||||
if (getWallet() != null) getWallet().setDaemonConnection(connection);
|
||||
updateSyncing();
|
||||
MoneroWallet wallet = getWallet();
|
||||
if (wallet == null) return;
|
||||
log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri());
|
||||
wallet.setDaemonConnection(connection);
|
||||
|
||||
// sync and reprocess messages on new thread
|
||||
new Thread(() -> {
|
||||
updateSyncing();
|
||||
|
||||
// reprocess pending payout messages
|
||||
this.getProtocol().maybeReprocessPaymentReceivedMessage(false);
|
||||
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void updateSyncing() {
|
||||
if (!isIdling()) syncWallet();
|
||||
if (!isIdling()) trySyncWallet();
|
||||
else {
|
||||
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
||||
UserThread.runAfter(() -> {
|
||||
if (isInitialized) syncWallet();
|
||||
if (isInitialized) trySyncWallet();
|
||||
}, startSyncingInMs / 1000l);
|
||||
}
|
||||
}
|
||||
@ -1525,7 +1588,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
if (isPayoutUnlocked()) return;
|
||||
|
||||
// rescan spent if deposits unlocked
|
||||
if (isDepositUnlocked()) getWallet().rescanSpent();
|
||||
if (isDepositsUnlocked()) getWallet().rescanSpent();
|
||||
|
||||
// get txs with outputs
|
||||
List<MoneroTxWallet> txs;
|
||||
@ -1538,7 +1601,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
// check deposit txs
|
||||
if (!isDepositUnlocked()) {
|
||||
if (!isDepositsUnlocked()) {
|
||||
if (txs.size() == 2) {
|
||||
setStateDepositsPublished();
|
||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||
@ -1585,19 +1648,22 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
private boolean isIdling() {
|
||||
return this instanceof ArbitratorTrade && isDepositConfirmed(); // arbitrator idles trade after deposits confirm
|
||||
return this instanceof ArbitratorTrade && isDepositsConfirmed(); // arbitrator idles trade after deposits confirm
|
||||
}
|
||||
|
||||
private void setStateDepositsPublished() {
|
||||
if (!isDepositPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
|
||||
if (!isDepositsPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
|
||||
}
|
||||
|
||||
private void setStateDepositsConfirmed() {
|
||||
if (!isDepositConfirmed()) setState(State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN);
|
||||
if (!isDepositsConfirmed()) setState(State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN);
|
||||
}
|
||||
|
||||
private void setStateDepositsUnlocked() {
|
||||
if (!isDepositUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||
if (!isDepositsUnlocked()) {
|
||||
setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||
setStartTimeFromUnlockedTxs();
|
||||
}
|
||||
}
|
||||
|
||||
private void setPayoutStatePublished() {
|
||||
@ -1634,6 +1700,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
||||
.collect(Collectors.toList()))
|
||||
.setLockTime(lockTime)
|
||||
.setStartTime(startTime)
|
||||
.setUid(uid);
|
||||
|
||||
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
|
||||
@ -1668,6 +1735,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
|
||||
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
|
||||
trade.setLockTime(proto.getLockTime());
|
||||
trade.setStartTime(proto.getStartTime());
|
||||
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
|
||||
|
||||
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
|
||||
@ -1722,6 +1790,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
",\n mediationResultState=" + mediationResultState +
|
||||
",\n mediationResultStateProperty=" + mediationResultStateProperty +
|
||||
",\n lockTime=" + lockTime +
|
||||
",\n startTime=" + startTime +
|
||||
",\n refundResultState=" + refundResultState +
|
||||
",\n refundResultStateProperty=" + refundResultStateProperty +
|
||||
"\n}";
|
||||
|
@ -369,6 +369,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
||||
});
|
||||
|
||||
// notify that persisted trades initialized
|
||||
persistedTradesInitialized.set(true);
|
||||
|
||||
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
||||
@ -1100,7 +1101,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
}
|
||||
|
||||
private void scheduleDeletionIfUnfunded(Trade trade) {
|
||||
if (trade.isDepositRequested() && !trade.isDepositPublished()) {
|
||||
if (trade.isDepositRequested() && !trade.isDepositsPublished()) {
|
||||
log.warn("Scheduling to delete trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
UserThread.runAfter(() -> {
|
||||
if (isShutDown) return;
|
||||
|
@ -23,10 +23,8 @@ import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@ -124,7 +122,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
|
||||
}
|
||||
|
||||
public static NetworkEnvelope fromProto(protobuf.PaymentReceivedMessage proto, String messageVersion) {
|
||||
public static PaymentReceivedMessage fromProto(protobuf.PaymentReceivedMessage proto, String messageVersion) {
|
||||
// There is no method to check for a nullable non-primitive data type object but we know that all fields
|
||||
// are empty/null, so we check for the signature to see if we got a valid buyerSignedWitness.
|
||||
protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getBuyerAccountAgeWitness();
|
||||
|
@ -20,14 +20,12 @@ package bisq.core.trade.messages;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
|
||||
import bisq.network.p2p.DirectMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.util.Utilities;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
@ -55,7 +55,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||
|
||||
// re-send payment sent message if not arrived
|
||||
synchronized (trade) {
|
||||
if (trade.getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && trade.getState().ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) {
|
||||
if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) {
|
||||
latchTrade();
|
||||
given(anyPhase(Trade.Phase.PAYMENT_SENT)
|
||||
.with(BuyerEvent.STARTUP))
|
||||
|
@ -31,10 +31,13 @@ import bisq.core.payment.payload.PaymentAccountPayload;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.dispute.messages.DisputeClosedMessage;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.MakerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.PaymentSentMessage;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.core.trade.statistics.ReferralIdService;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
@ -43,7 +46,7 @@ import bisq.core.user.User;
|
||||
import bisq.network.p2p.AckMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
@ -175,6 +178,18 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean isDepositsConfirmedMessagesDelivered;
|
||||
@Nullable
|
||||
@Setter
|
||||
@Getter
|
||||
private PaymentSentMessage paymentSentMessage;
|
||||
@Nullable
|
||||
@Setter
|
||||
@Getter
|
||||
private PaymentReceivedMessage paymentReceivedMessage;
|
||||
@Nullable
|
||||
@Setter
|
||||
@Getter
|
||||
private DisputeClosedMessage disputeClosedMessage;
|
||||
|
||||
// We want to indicate the user the state of the message delivery of the
|
||||
// PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet.
|
||||
@ -233,6 +248,9 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||
Optional.ofNullable(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage()));
|
||||
Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(makerSignature));
|
||||
Optional.ofNullable(multisigAddress).ifPresent(e -> builder.setMultisigAddress(multisigAddress));
|
||||
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||
Optional.ofNullable(paymentReceivedMessage).ifPresent(e -> builder.setPaymentReceivedMessage(paymentReceivedMessage.toProtoNetworkEnvelope().getPaymentReceivedMessage()));
|
||||
Optional.ofNullable(disputeClosedMessage).ifPresent(e -> builder.setDisputeClosedMessage(disputeClosedMessage.toProtoNetworkEnvelope().getDisputeClosedMessage()));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@ -267,6 +285,9 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
||||
processModel.setPaymentStartedMessageState(paymentStartedMessageState);
|
||||
|
||||
processModel.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null);
|
||||
processModel.setPaymentReceivedMessage(proto.hasPaymentReceivedMessage() ? PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), Version.getP2PMessageVersion()) : null);
|
||||
processModel.setDisputeClosedMessage(proto.hasDisputeClosedMessage() ? DisputeClosedMessage.fromProto(proto.getDisputeClosedMessage(), Version.getP2PMessageVersion()) : null);
|
||||
return processModel;
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ public class SellerProtocol extends DisputeProtocol {
|
||||
|
||||
// re-send payment received message if not arrived
|
||||
synchronized (trade) {
|
||||
if (trade.getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && trade.getState().ordinal() <= Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.getState().ordinal() <= Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||
latchTrade();
|
||||
given(anyPhase(Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(SellerEvent.STARTUP))
|
||||
|
@ -74,7 +74,6 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
@ -94,6 +93,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
protected TradeResultHandler tradeResultHandler;
|
||||
protected ErrorMessageHandler errorMessageHandler;
|
||||
|
||||
private int reprocessPaymentReceivedMessageCount;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -267,6 +267,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// reprocess payout messages if pending
|
||||
maybeReprocessPaymentReceivedMessage(true);
|
||||
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(trade, true);
|
||||
}
|
||||
|
||||
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
||||
synchronized (trade) {
|
||||
|
||||
// skip if no need to reprocess
|
||||
if (trade.isSeller() || trade.getProcessModel().getPaymentReceivedMessage() == null || trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn("Reprocessing payment received message for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
new Thread(() -> handle(trade.getProcessModel().getPaymentReceivedMessage(), trade.getProcessModel().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError)).start();
|
||||
}
|
||||
}
|
||||
|
||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||
@ -462,17 +479,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
|
||||
// received by buyer and arbitrator
|
||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
||||
handle(message, peer, true);
|
||||
}
|
||||
|
||||
private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) {
|
||||
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
|
||||
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {
|
||||
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
||||
return;
|
||||
}
|
||||
if (trade instanceof ArbitratorTrade && !trade.isPayoutUnlocked()) trade.syncWallet(); // arbitrator syncs slowly after deposits confirmed
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
expect(anyPhase(trade.isBuyer() ? new Trade.Phase[] {Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED} : new Trade.Phase[] {Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT})
|
||||
expect(anyPhase(
|
||||
trade.isBuyer() ? new Trade.Phase[] {Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED} :
|
||||
trade.isArbitrator() ? new Trade.Phase[] {Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT} : // arbitrator syncs slowly after deposits confirmed
|
||||
new Trade.Phase[] {Trade.Phase.DEPOSITS_UNLOCKED, Trade.Phase.PAYMENT_SENT})
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
@ -482,7 +505,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
log.warn("Error processing payment received message: " + errorMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// reprocess message depending on error
|
||||
if (trade.getProcessModel().getPaymentReceivedMessage() != null) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentReceivedMessageCount++;
|
||||
maybeReprocessPaymentReceivedMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount));
|
||||
} else {
|
||||
handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack
|
||||
}
|
||||
unlatchTrade();
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
@ -548,8 +583,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
private void onAckMessage(AckMessage ackMessage, NodeAddress peer) {
|
||||
// We handle the ack for PaymentSentMessage and DepositTxAndDelayedPayoutTxMessage
|
||||
// as we support automatic re-send of the msg in case it was not ACKed after a certain time
|
||||
// TODO (woodser): add AckMessage for InitTradeRequest and support automatic re-send ?
|
||||
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
|
||||
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName()) && trade.getTradingPeer(peer) == trade.getSeller()) {
|
||||
processModel.setPaymentStartedAckMessage(ackMessage);
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,7 @@ import bisq.core.account.witness.AccountAgeWitness;
|
||||
import bisq.core.btc.model.RawTransactionInput;
|
||||
import bisq.core.payment.payload.PaymentAccountPayload;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.trade.messages.PaymentSentMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.proto.persistable.PersistablePayload;
|
||||
@ -131,8 +129,6 @@ public final class TradingPeer implements PersistablePayload {
|
||||
private String depositTxKey;
|
||||
@Nullable
|
||||
private String updatedMultisigHex;
|
||||
@Nullable
|
||||
private PaymentSentMessage paymentSentMessage;
|
||||
|
||||
public TradingPeer() {
|
||||
}
|
||||
@ -173,7 +169,6 @@ public final class TradingPeer implements PersistablePayload {
|
||||
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
|
||||
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
|
||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
||||
|
||||
builder.setCurrentDate(currentDate);
|
||||
return builder.build();
|
||||
@ -224,7 +219,6 @@ public final class TradingPeer implements PersistablePayload {
|
||||
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
|
||||
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
|
||||
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
||||
tradingPeer.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null);
|
||||
return tradingPeer;
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,13 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// skip if already created
|
||||
if (processModel.getPaymentSentMessage() != null) {
|
||||
log.warn("Skipping preparation of payment sent message since it's already created for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
// validate state
|
||||
Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null");
|
||||
Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null");
|
||||
@ -67,7 +74,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) {
|
||||
if (!updatedMultisigHexes.isEmpty()) {
|
||||
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||
trade.saveWallet();
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
|
||||
|
||||
@Override
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||
if (trade.getSelf().getPaymentSentMessage() == null) {
|
||||
if (processModel.getPaymentSentMessage() == null) {
|
||||
|
||||
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
||||
// peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox
|
||||
@ -99,12 +99,13 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
|
||||
String messageAsJson = JsonUtil.objectToJson(message);
|
||||
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
||||
message.setBuyerSignature(sig);
|
||||
trade.getSelf().setPaymentSentMessage(message);
|
||||
processModel.setPaymentSentMessage(message);
|
||||
trade.requestPersistence();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException (e);
|
||||
}
|
||||
}
|
||||
return trade.getSelf().getPaymentSentMessage();
|
||||
return processModel.getPaymentSentMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@Slf4j
|
||||
public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
@ -46,6 +48,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
log.debug("current trade state " + trade.getState());
|
||||
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
|
||||
checkNotNull(message);
|
||||
@ -54,6 +57,12 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
|
||||
// verify signature of payment received message
|
||||
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
|
||||
|
||||
// save message for reprocessing
|
||||
processModel.setPaymentReceivedMessage(message);
|
||||
trade.requestPersistence();
|
||||
|
||||
// set state
|
||||
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
||||
trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness());
|
||||
@ -63,13 +72,16 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
|
||||
|
||||
// close open disputes
|
||||
if (trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_OPENED.ordinal()) {
|
||||
if (trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_REQUESTED.ordinal()) {
|
||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_CLOSED);
|
||||
for (Dispute dispute : trade.getDisputes()) {
|
||||
dispute.setIsClosed();
|
||||
}
|
||||
}
|
||||
|
||||
// ensure connected to monero network
|
||||
trade.checkWalletConnection();
|
||||
|
||||
// process payout tx unless already unlocked
|
||||
if (!trade.isPayoutUnlocked()) processPayoutTx(message);
|
||||
|
||||
@ -83,25 +95,32 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
|
||||
// complete
|
||||
trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator auto completes when payout published
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
trade.requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
|
||||
// do not reprocess illegal argument
|
||||
if (t instanceof IllegalArgumentException) {
|
||||
processModel.setPaymentReceivedMessage(null); // do not reprocess
|
||||
trade.requestPersistence();
|
||||
}
|
||||
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void processPayoutTx(PaymentReceivedMessage message) {
|
||||
|
||||
// sync and save wallet
|
||||
trade.syncWallet();
|
||||
trade.saveWallet();
|
||||
|
||||
// import multisig hex
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) trade.getWallet().importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||
|
||||
// sync and save wallet
|
||||
trade.syncWallet();
|
||||
trade.saveWallet();
|
||||
|
||||
// handle if payout tx not published
|
||||
if (!trade.isPayoutPublished()) {
|
||||
|
||||
@ -110,18 +129,23 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
|
||||
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||
trade.syncWallet();
|
||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||
}
|
||||
|
||||
// verify and publish payout tx
|
||||
if (!trade.isPayoutPublished()) {
|
||||
if (isSigned) {
|
||||
log.info("{} publishing signed payout tx from seller", trade.getClass().getSimpleName());
|
||||
log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId());
|
||||
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
||||
} else {
|
||||
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
|
||||
try {
|
||||
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
||||
if (StringUtils.equals(trade.getPayoutTxHex(), trade.getProcessModel().getPaymentSentMessage().getPayoutTxHex())) { // unsigned
|
||||
log.info("{} {} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName(), trade.getId());
|
||||
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
||||
} else {
|
||||
log.info("{} {} re-verifying and publishing payout tx", trade.getClass().getSimpleName(), trade.getId());
|
||||
trade.verifyPayoutTx(trade.getPayoutTxHex(), false, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
||||
else throw e;
|
||||
|
@ -44,10 +44,10 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
||||
// verify signature of payment sent message
|
||||
HavenoUtils.verifyPaymentSentMessage(trade, message);
|
||||
|
||||
// update buyer info
|
||||
// set state
|
||||
processModel.setPaymentSentMessage(message);
|
||||
trade.setPayoutTxHex(message.getPayoutTxHex());
|
||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
trade.getBuyer().setPaymentSentMessage(message);
|
||||
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
|
||||
|
||||
// if seller, decrypt buyer's payment account payload
|
||||
@ -62,7 +62,7 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
||||
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
||||
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
||||
trade.setStateIfProgress(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
trade.requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -42,27 +41,39 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// import multisig hex
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) {
|
||||
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0]));
|
||||
trade.saveWallet();
|
||||
}
|
||||
// check connection
|
||||
trade.checkWalletConnection();
|
||||
|
||||
// verify, sign, and publish payout tx if given. otherwise create payout tx
|
||||
if (trade.getPayoutTxHex() != null) {
|
||||
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
|
||||
trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true);
|
||||
} else {
|
||||
// handle first time preparation
|
||||
if (processModel.getPaymentReceivedMessage() == null) {
|
||||
|
||||
// create unsigned payout tx
|
||||
log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
|
||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||
trade.setPayoutTx(payoutTx);
|
||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
// import multisig hex
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) {
|
||||
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0]));
|
||||
trade.saveWallet();
|
||||
}
|
||||
|
||||
// verify, sign, and publish payout tx if given. otherwise create payout tx
|
||||
if (trade.getPayoutTxHex() != null) {
|
||||
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
|
||||
trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true);
|
||||
} else {
|
||||
|
||||
// create unsigned payout tx
|
||||
log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
|
||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||
trade.setPayoutTx(payoutTx);
|
||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
}
|
||||
} else if (processModel.getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
|
||||
|
||||
// republish payout tx from previous message
|
||||
log.info("Seller re-verifying and publishing payout tx for trade {}", trade.getId());
|
||||
trade.verifyPayoutTx(processModel.getPaymentReceivedMessage().getSignedPayoutTxHex(), false, true);
|
||||
}
|
||||
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
@ -39,8 +39,8 @@ import com.google.common.base.Charsets;
|
||||
@Slf4j
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||
SignedWitness signedWitness = null;
|
||||
PaymentReceivedMessage message = null;
|
||||
SignedWitness signedWitness = null;
|
||||
|
||||
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
@ -87,7 +87,7 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
||||
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
|
||||
trade.getTradingPeer().getAccountAgeWitness(),
|
||||
signedWitness,
|
||||
trade.getBuyer().getPaymentSentMessage()
|
||||
processModel.getPaymentSentMessage()
|
||||
);
|
||||
|
||||
// sign message
|
||||
@ -95,6 +95,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
||||
String messageAsJson = JsonUtil.objectToJson(message);
|
||||
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
||||
message.setSellerSignature(sig);
|
||||
processModel.setPaymentReceivedMessage(message);
|
||||
trade.requestPersistence();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
@ -34,6 +36,15 @@ public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentRe
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||
if (processModel.getPaymentReceivedMessage() == null) {
|
||||
processModel.setPaymentReceivedMessage((PaymentReceivedMessage) super.getTradeMailboxMessage(tradeId)); // save payment received message for buyer
|
||||
}
|
||||
return processModel.getPaymentReceivedMessage();
|
||||
}
|
||||
|
||||
protected NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getBuyer().getNodeAddress();
|
||||
}
|
||||
|
@ -1139,6 +1139,7 @@ support.role=Role
|
||||
support.agent=Support agent
|
||||
support.state=State
|
||||
support.chat=Chat
|
||||
support.requested=Requested
|
||||
support.closed=Closed
|
||||
support.open=Open
|
||||
support.process=Process
|
||||
@ -1967,6 +1968,7 @@ tradeDetailsWindow.txFee=Mining fee
|
||||
tradeDetailsWindow.tradingPeersOnion=Trading peers onion address
|
||||
tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash
|
||||
tradeDetailsWindow.tradeState=Trade state
|
||||
tradeDetailsWindow.tradePhase=Trade phase
|
||||
tradeDetailsWindow.agentAddresses=Arbitrator/Mediator
|
||||
tradeDetailsWindow.detailData=Detail data
|
||||
|
||||
|
@ -409,7 +409,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||
placeOfferHandlerOptional.ifPresent(Runnable::run);
|
||||
} else {
|
||||
State lastState = Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS;
|
||||
spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo") + " 1/" + (lastState.ordinal()));
|
||||
spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo") + " 1/" + (lastState.ordinal() + 1));
|
||||
takeOfferHandlerOptional.ifPresent(Runnable::run);
|
||||
|
||||
// update trade state progress
|
||||
@ -417,7 +417,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||
Trade trade = tradeManager.getTrade(offer.getId());
|
||||
if (trade == null) return;
|
||||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newState -> {
|
||||
String progress = (newState.ordinal() + 1) + "/" + (lastState.ordinal());
|
||||
String progress = (newState.ordinal() + 1) + "/" + (lastState.ordinal() + 1);
|
||||
spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo") + " " + progress);
|
||||
|
||||
// unsubscribe when done
|
||||
|
@ -299,7 +299,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||
textArea.scrollTopProperty().addListener(changeListener);
|
||||
textArea.setScrollTop(30);
|
||||
|
||||
addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradeState"), trade.getPhase().name());
|
||||
addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePhase"), trade.getPhase().name());
|
||||
}
|
||||
|
||||
Tuple3<Button, Button, HBox> tuple = add2ButtonsWithBox(gridPane, ++rowIndex,
|
||||
@ -322,10 +322,13 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||
viewContractButton.setOnAction(e -> {
|
||||
TextArea textArea = new HavenoTextArea();
|
||||
textArea.setText(trade.getContractAsJson());
|
||||
String data = "Contract as json:\n";
|
||||
String data = "Trade state: " + trade.getState();
|
||||
data += "\nTrade payout state: " + trade.getPayoutState();
|
||||
data += "\nTrade dispute state: " + trade.getDisputeState();
|
||||
data += "\n\nContract as json:\n";
|
||||
data += trade.getContractAsJson();
|
||||
data += "\n\nOther detail data:";
|
||||
if (!trade.isDepositPublished()) {
|
||||
if (!trade.isDepositsPublished()) {
|
||||
data += "\n\n" + (trade.getMaker() == trade.getBuyer() ? "Buyer" : "Seller") + " as maker reserve tx hex: " + trade.getMaker().getReserveTxHex();
|
||||
data += "\n\n" + (trade.getTaker() == trade.getBuyer() ? "Buyer" : "Seller") + " as taker reserve tx hex: " + trade.getTaker().getReserveTxHex();
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ import bisq.core.provider.mempool.MempoolService;
|
||||
import bisq.core.trade.ArbitratorTrade;
|
||||
import bisq.core.trade.BuyerTrade;
|
||||
import bisq.core.trade.ClosedTradableManager;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.trade.SellerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
@ -433,21 +432,19 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
buyerState.set(BuyerState.STEP2);
|
||||
break;
|
||||
|
||||
// seller step 3
|
||||
case SELLER_RECEIVED_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG received
|
||||
sellerState.set(SellerState.STEP3);
|
||||
break;
|
||||
|
||||
// seller step 4
|
||||
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: // UI action
|
||||
// payment received
|
||||
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
|
||||
if (trade instanceof BuyerTrade) buyerState.set(BuyerState.STEP4);
|
||||
else if (trade instanceof SellerTrade) sellerState.set(SellerState.STEP3);
|
||||
else if (trade instanceof SellerTrade) sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
|
||||
break;
|
||||
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
|
||||
|
||||
// seller step 3
|
||||
case SELLER_RECEIVED_PAYMENT_SENT_MSG: // PAYMENT_SENT_MSG received
|
||||
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT:
|
||||
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
|
||||
sellerState.set(SellerState.STEP4);
|
||||
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
|
||||
sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
|
||||
break;
|
||||
|
||||
case TRADE_COMPLETED:
|
||||
|
@ -801,8 +801,9 @@ public abstract class TradeStepView extends AnchorPane {
|
||||
// }
|
||||
// }
|
||||
|
||||
protected void checkForTimeout() {
|
||||
long unconfirmedHours = Duration.between(trade.getTakeOfferDate().toInstant(), Instant.now()).toHours();
|
||||
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)) {
|
||||
|
@ -37,7 +37,7 @@ public class BuyerStep1View extends TradeStepView {
|
||||
super.onPendingTradesInitialized();
|
||||
//validatePayoutTx(); // TODO (woodser): no payout tx in xmr integration, do something else?
|
||||
//validateDepositInputs();
|
||||
checkForTimeout();
|
||||
checkForUnconfirmedTimeout();
|
||||
}
|
||||
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package bisq.desktop.main.portfolio.pendingtrades.steps.buyer;
|
||||
|
||||
import bisq.desktop.components.AutoTooltipButton;
|
||||
import bisq.desktop.components.BusyAnimation;
|
||||
import bisq.desktop.components.TextFieldWithCopyIcon;
|
||||
import bisq.desktop.components.TitledGroupBg;
|
||||
@ -155,7 +154,7 @@ public class BuyerStep2View extends TradeStepView {
|
||||
if (timeoutTimer != null)
|
||||
timeoutTimer.stop();
|
||||
|
||||
if (trade.isDepositUnlocked() && !trade.isPaymentSent()) {
|
||||
if (trade.isDepositsUnlocked() && !trade.isPaymentSent()) {
|
||||
showPopup();
|
||||
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) {
|
||||
if (!trade.hasFailed()) {
|
||||
@ -481,6 +480,10 @@ public class BuyerStep2View extends TradeStepView {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!model.dataModel.isReadyForTxBroadcast()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PaymentAccountPayload sellersPaymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
|
||||
Trade trade = checkNotNull(model.dataModel.getTrade(), "trade must not be null");
|
||||
if (sellersPaymentAccountPayload instanceof CashDepositAccountPayload) {
|
||||
|
@ -37,7 +37,7 @@ public class SellerStep1View extends TradeStepView {
|
||||
super.onPendingTradesInitialized();
|
||||
//validateDepositInputs();
|
||||
log.warn("Need to validate fee and/or deposit txs in SellerStep1View for XMR?"); // TODO (woodser): need to validate fee and/or deposit txs in SellerStep1View?
|
||||
checkForTimeout();
|
||||
checkForUnconfirmedTimeout();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -306,11 +306,16 @@ public class SellerStep3View extends TradeStepView {
|
||||
HBox hBox = tuple.fourth;
|
||||
GridPane.setColumnSpan(tuple.fourth, 2);
|
||||
confirmButton = tuple.first;
|
||||
confirmButton.setDisable(!confirmPaymentReceivedPermitted());
|
||||
confirmButton.setOnAction(e -> onPaymentReceived());
|
||||
busyAnimation = tuple.second;
|
||||
statusLabel = tuple.third;
|
||||
}
|
||||
|
||||
private boolean confirmPaymentReceivedPermitted() {
|
||||
if (!trade.confirmPermitted()) return false;
|
||||
return trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal(); // TODO: test that can resen with same payout tx hex if delivery failed
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Info
|
||||
@ -357,7 +362,7 @@ public class SellerStep3View extends TradeStepView {
|
||||
protected void updateDisputeState(Trade.DisputeState disputeState) {
|
||||
super.updateDisputeState(disputeState);
|
||||
|
||||
confirmButton.setDisable(!trade.confirmPermitted());
|
||||
confirmButton.setDisable(!confirmPaymentReceivedPermitted());
|
||||
}
|
||||
|
||||
|
||||
@ -463,11 +468,14 @@ public class SellerStep3View extends TradeStepView {
|
||||
log.info("User pressed the [Confirm payment receipt] button for Trade {}", trade.getShortId());
|
||||
busyAnimation.play();
|
||||
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
||||
confirmButton.setDisable(true);
|
||||
|
||||
model.dataModel.onPaymentReceived(() -> {
|
||||
}, errorMessage -> {
|
||||
busyAnimation.stop();
|
||||
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show();
|
||||
confirmButton.setDisable(!confirmPaymentReceivedPermitted());
|
||||
UserThread.execute(() -> statusLabel.setText("Error confirming payment received."));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,7 @@ import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.Trade.DisputeState;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
@ -1341,18 +1342,21 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||
|
||||
ReadOnlyBooleanProperty closedProperty;
|
||||
ChangeListener<Boolean> listener;
|
||||
Subscription subscription;
|
||||
|
||||
@Override
|
||||
public void updateItem(final Dispute item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
UserThread.execute(() -> {
|
||||
if (item != null && !empty) {
|
||||
if (closedProperty != null) {
|
||||
closedProperty.removeListener(listener);
|
||||
if (closedProperty != null) closedProperty.removeListener(listener);
|
||||
if (subscription != null) {
|
||||
subscription.unsubscribe();
|
||||
subscription = null;
|
||||
}
|
||||
|
||||
listener = (observable, oldValue, newValue) -> {
|
||||
setText(newValue ? Res.get("support.closed") : Res.get("support.open"));
|
||||
setText(getDisputeStateText(item));
|
||||
if (getTableRow() != null)
|
||||
getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
||||
if (item.isClosed() && item == chatPopup.getSelectedDispute())
|
||||
@ -1361,14 +1365,23 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||
closedProperty = item.isClosedProperty();
|
||||
closedProperty.addListener(listener);
|
||||
boolean isClosed = item.isClosed();
|
||||
setText(isClosed ? Res.get("support.closed") : Res.get("support.open"));
|
||||
setText(getDisputeStateText(item));
|
||||
if (getTableRow() != null)
|
||||
getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
||||
|
||||
// subscribe to trade's dispute state
|
||||
Trade trade = tradeManager.getTrade(item.getTradeId());
|
||||
if (trade == null) log.warn("Dispute's trade is null for trade {}", item.getTradeId());
|
||||
else subscription = EasyBind.subscribe(trade.disputeStateProperty(), disputeState -> setText(getDisputeStateText(disputeState)));
|
||||
} else {
|
||||
if (closedProperty != null) {
|
||||
closedProperty.removeListener(listener);
|
||||
closedProperty = null;
|
||||
}
|
||||
if (subscription != null) {
|
||||
subscription.unsubscribe();
|
||||
subscription = null;
|
||||
}
|
||||
setText("");
|
||||
}
|
||||
});
|
||||
@ -1379,6 +1392,33 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||
return column;
|
||||
}
|
||||
|
||||
private String getDisputeStateText(DisputeState disputeState) {
|
||||
switch (disputeState) {
|
||||
case DISPUTE_REQUESTED:
|
||||
return Res.get("support.requested");
|
||||
case DISPUTE_CLOSED:
|
||||
return Res.get("support.closed");
|
||||
default:
|
||||
return Res.get("support.open");
|
||||
}
|
||||
}
|
||||
|
||||
private String getDisputeStateText(Dispute dispute) {
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
if (trade == null) {
|
||||
log.warn("Dispute's trade is null for trade {}", dispute.getTradeId());
|
||||
return Res.get("support.closed");
|
||||
}
|
||||
switch (trade.getDisputeState()) {
|
||||
case DISPUTE_REQUESTED:
|
||||
return Res.get("support.requested");
|
||||
case DISPUTE_CLOSED:
|
||||
return Res.get("support.closed");
|
||||
default:
|
||||
return Res.get("support.open");
|
||||
}
|
||||
}
|
||||
|
||||
private void openChat(Dispute dispute) {
|
||||
chatPopup.openChat(dispute, getConcreteDisputeChatSession(dispute), getCounterpartyName());
|
||||
dispute.setDisputeSeen(senderFlag());
|
||||
|
@ -738,11 +738,18 @@ public class GUIUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
connectionService.verifyConnection();
|
||||
} catch (Exception e) {
|
||||
new Popup().information(e.getMessage()).show();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isChainHeightSyncedWithinToleranceOrShowPopup(CoreMoneroConnectionsService connectionService) {
|
||||
if (!connectionService.isChainHeightSyncedWithinTolerance()) {
|
||||
if (!connectionService.isSyncedWithinTolerance()) {
|
||||
new Popup().information(Res.get("popup.warning.chainNotSynced")).show();
|
||||
return false;
|
||||
}
|
||||
|
14
docs/operation_manual.md
Normal file
14
docs/operation_manual.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Operation Manual
|
||||
|
||||
This operation manual describes how to operate a Haveno network by:
|
||||
|
||||
- Forking Haveno
|
||||
- Creating and registering seed nodes
|
||||
- Creating and registering arbitrators
|
||||
- Building binaries of the application
|
||||
|
||||
TODO
|
||||
|
||||
## Manually open dispute by keyboard shortcut
|
||||
|
||||
In the event a dispute does not open properly, try manually reopening the dispute with a keyboard shortcut: `ctrl+o`
|
@ -841,9 +841,9 @@ message TradeInfo {
|
||||
string period_state = 19;
|
||||
string payout_state = 20;
|
||||
string dispute_state = 21;
|
||||
bool is_deposit_published = 22;
|
||||
bool is_deposit_confirmed = 23;
|
||||
bool is_deposit_unlocked = 24;
|
||||
bool is_deposits_published = 22;
|
||||
bool is_deposits_confirmed = 23;
|
||||
bool is_deposits_unlocked = 24;
|
||||
bool is_payment_sent = 25;
|
||||
bool is_payment_received = 26;
|
||||
bool is_payout_published = 27;
|
||||
|
@ -1652,11 +1652,12 @@ message Trade {
|
||||
repeated ChatMessage chat_message = 22;
|
||||
MediationResultState mediation_result_state = 23;
|
||||
int64 lock_time = 24;
|
||||
NodeAddress refund_agent_node_address = 25;
|
||||
RefundResultState refund_result_state = 26;
|
||||
string counter_currency_extra_data = 27;
|
||||
string asset_tx_proof_result = 28; // name of AssetTxProofResult enum
|
||||
string uid = 29;
|
||||
int64 start_time = 25;
|
||||
NodeAddress refund_agent_node_address = 26;
|
||||
RefundResultState refund_result_state = 27;
|
||||
string counter_currency_extra_data = 28;
|
||||
string asset_tx_proof_result = 29; // name of AssetTxProofResult enum
|
||||
string uid = 30;
|
||||
}
|
||||
|
||||
message BuyerAsMakerTrade {
|
||||
@ -1708,6 +1709,10 @@ message ProcessModel {
|
||||
TradingPeer arbitrator = 1004;
|
||||
NodeAddress temp_trading_peer_node_address = 1005;
|
||||
string multisig_address = 1006;
|
||||
|
||||
PaymentSentMessage payment_sent_message = 1012;
|
||||
PaymentReceivedMessage payment_received_message = 1013;
|
||||
DisputeClosedMessage dispute_closed_message = 1014;
|
||||
}
|
||||
|
||||
message TradingPeer {
|
||||
@ -1745,7 +1750,6 @@ message TradingPeer {
|
||||
string deposit_tx_hex = 1009;
|
||||
string deposit_tx_key = 1010;
|
||||
string updated_multisig_hex = 1011;
|
||||
PaymentSentMessage payment_sent_message = 1012;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
Loading…
Reference in New Issue
Block a user