mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 11:39:29 +00: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
49 changed files with 841 additions and 471 deletions
|
@ -109,7 +109,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final void verifyTakerDepositConfirmed(TradeInfo trade) {
|
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.",
|
fail(format("INVALID_PHASE for trade %s in STATE=%s PHASE=%s, deposit tx never unlocked.",
|
||||||
trade.getShortId(),
|
trade.getShortId(),
|
||||||
trade.getState(),
|
trade.getState(),
|
||||||
|
@ -182,9 +182,9 @@ public class AbstractTradeTest extends AbstractOfferTest {
|
||||||
assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase());
|
assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase());
|
||||||
|
|
||||||
if (!isLongRunningTest)
|
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.isPaymentStartedMessageSent, trade.getIsPaymentSent());
|
||||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived());
|
assertEquals(EXPECTED_PROTOCOL_STATUS.isPaymentReceivedMessageSent, trade.getIsPaymentReceived());
|
||||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished());
|
assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished());
|
||||||
|
|
|
@ -231,7 +231,7 @@ public class BotClient {
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public boolean isTakerDepositFeeTxConfirmed(String tradeId) {
|
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) -> {
|
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());
|
log.info("Taker deposit fee tx {} has been published.", trade.getTakerDepositTxId());
|
||||||
return true;
|
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());
|
log.info("Taker deposit fee tx {} has been confirmed.", trade.getTakerDepositTxId());
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -65,8 +65,8 @@ class TradeDetailTableBuilder extends AbstractTradeListBuilder {
|
||||||
colAmount.addRow(toTradeAmount.apply(trade));
|
colAmount.addRow(toTradeAmount.apply(trade));
|
||||||
colMinerTxFee.addRow(toMyMinerTxFee.apply(trade));
|
colMinerTxFee.addRow(toMyMinerTxFee.apply(trade));
|
||||||
colBisqTradeFee.addRow(toMyMakerOrTakerFee.apply(trade));
|
colBisqTradeFee.addRow(toMyMakerOrTakerFee.apply(trade));
|
||||||
colIsDepositPublished.addRow(trade.getIsDepositPublished());
|
colIsDepositPublished.addRow(trade.getIsDepositsPublished());
|
||||||
colIsDepositConfirmed.addRow(trade.getIsDepositUnlocked());
|
colIsDepositConfirmed.addRow(trade.getIsDepositsUnlocked());
|
||||||
colTradeCost.addRow(toTradeVolumeAsString.apply(trade));
|
colTradeCost.addRow(toTradeVolumeAsString.apply(trade));
|
||||||
colIsPaymentStartedMessageSent.addRow(trade.getIsPaymentSent());
|
colIsPaymentStartedMessageSent.addRow(trade.getIsPaymentSent());
|
||||||
colIsPaymentReceivedMessageSent.addRow(trade.getIsPaymentReceived());
|
colIsPaymentReceivedMessageSent.addRow(trade.getIsPaymentReceived());
|
||||||
|
|
|
@ -164,7 +164,7 @@ public class CoreAccountService {
|
||||||
|
|
||||||
public void deleteAccount(Runnable onShutdown) {
|
public void deleteAccount(Runnable onShutdown) {
|
||||||
try {
|
try {
|
||||||
keyRing.lockKeys();
|
if (isAccountOpen()) closeAccount();
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown);
|
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;
|
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
|
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
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------- APP METHODS ------------------------------
|
||||||
|
|
||||||
public ReadOnlyIntegerProperty numPeersProperty() {
|
public ReadOnlyIntegerProperty numPeersProperty() {
|
||||||
return numPeers;
|
return numPeers;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,9 +82,9 @@ public class TradeInfo implements Payload {
|
||||||
private final String periodState;
|
private final String periodState;
|
||||||
private final String payoutState;
|
private final String payoutState;
|
||||||
private final String disputeState;
|
private final String disputeState;
|
||||||
private final boolean isDepositPublished;
|
private final boolean isDepositsPublished;
|
||||||
private final boolean isDepositConfirmed;
|
private final boolean isDepositsConfirmed;
|
||||||
private final boolean isDepositUnlocked;
|
private final boolean isDepositsUnlocked;
|
||||||
private final boolean isPaymentSent;
|
private final boolean isPaymentSent;
|
||||||
private final boolean isPaymentReceived;
|
private final boolean isPaymentReceived;
|
||||||
private final boolean isPayoutPublished;
|
private final boolean isPayoutPublished;
|
||||||
|
@ -117,9 +117,9 @@ public class TradeInfo implements Payload {
|
||||||
this.periodState = builder.getPeriodState();
|
this.periodState = builder.getPeriodState();
|
||||||
this.payoutState = builder.getPayoutState();
|
this.payoutState = builder.getPayoutState();
|
||||||
this.disputeState = builder.getDisputeState();
|
this.disputeState = builder.getDisputeState();
|
||||||
this.isDepositPublished = builder.isDepositPublished();
|
this.isDepositsPublished = builder.isDepositsPublished();
|
||||||
this.isDepositConfirmed = builder.isDepositConfirmed();
|
this.isDepositsConfirmed = builder.isDepositsConfirmed();
|
||||||
this.isDepositUnlocked = builder.isDepositUnlocked();
|
this.isDepositsUnlocked = builder.isDepositsUnlocked();
|
||||||
this.isPaymentSent = builder.isPaymentSent();
|
this.isPaymentSent = builder.isPaymentSent();
|
||||||
this.isPaymentReceived = builder.isPaymentReceived();
|
this.isPaymentReceived = builder.isPaymentReceived();
|
||||||
this.isPayoutPublished = builder.isPayoutPublished();
|
this.isPayoutPublished = builder.isPayoutPublished();
|
||||||
|
@ -175,9 +175,9 @@ public class TradeInfo implements Payload {
|
||||||
.withPeriodState(trade.getPeriodState().name())
|
.withPeriodState(trade.getPeriodState().name())
|
||||||
.withPayoutState(trade.getPayoutState().name())
|
.withPayoutState(trade.getPayoutState().name())
|
||||||
.withDisputeState(trade.getDisputeState().name())
|
.withDisputeState(trade.getDisputeState().name())
|
||||||
.withIsDepositPublished(trade.isDepositPublished())
|
.withIsDepositsPublished(trade.isDepositsPublished())
|
||||||
.withIsDepositConfirmed(trade.isDepositConfirmed())
|
.withIsDepositsConfirmed(trade.isDepositsConfirmed())
|
||||||
.withIsDepositUnlocked(trade.isDepositUnlocked())
|
.withIsDepositsUnlocked(trade.isDepositsUnlocked())
|
||||||
.withIsPaymentSent(trade.isPaymentSent())
|
.withIsPaymentSent(trade.isPaymentSent())
|
||||||
.withIsPaymentReceived(trade.isPaymentReceived())
|
.withIsPaymentReceived(trade.isPaymentReceived())
|
||||||
.withIsPayoutPublished(trade.isPayoutPublished())
|
.withIsPayoutPublished(trade.isPayoutPublished())
|
||||||
|
@ -219,9 +219,9 @@ public class TradeInfo implements Payload {
|
||||||
.setPeriodState(periodState)
|
.setPeriodState(periodState)
|
||||||
.setPayoutState(payoutState)
|
.setPayoutState(payoutState)
|
||||||
.setDisputeState(disputeState)
|
.setDisputeState(disputeState)
|
||||||
.setIsDepositPublished(isDepositPublished)
|
.setIsDepositsPublished(isDepositsPublished)
|
||||||
.setIsDepositConfirmed(isDepositConfirmed)
|
.setIsDepositsConfirmed(isDepositsConfirmed)
|
||||||
.setIsDepositUnlocked(isDepositUnlocked)
|
.setIsDepositsUnlocked(isDepositsUnlocked)
|
||||||
.setIsPaymentSent(isPaymentSent)
|
.setIsPaymentSent(isPaymentSent)
|
||||||
.setIsPaymentReceived(isPaymentReceived)
|
.setIsPaymentReceived(isPaymentReceived)
|
||||||
.setIsCompleted(isCompleted)
|
.setIsCompleted(isCompleted)
|
||||||
|
@ -257,9 +257,9 @@ public class TradeInfo implements Payload {
|
||||||
.withPhase(proto.getPhase())
|
.withPhase(proto.getPhase())
|
||||||
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
|
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
|
||||||
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
|
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
|
||||||
.withIsDepositPublished(proto.getIsDepositPublished())
|
.withIsDepositsPublished(proto.getIsDepositsPublished())
|
||||||
.withIsDepositConfirmed(proto.getIsDepositConfirmed())
|
.withIsDepositsConfirmed(proto.getIsDepositsConfirmed())
|
||||||
.withIsDepositUnlocked(proto.getIsDepositUnlocked())
|
.withIsDepositsUnlocked(proto.getIsDepositsUnlocked())
|
||||||
.withIsPaymentSent(proto.getIsPaymentSent())
|
.withIsPaymentSent(proto.getIsPaymentSent())
|
||||||
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
||||||
.withIsCompleted(proto.getIsCompleted())
|
.withIsCompleted(proto.getIsCompleted())
|
||||||
|
@ -294,9 +294,9 @@ public class TradeInfo implements Payload {
|
||||||
", periodState='" + periodState + '\'' + "\n" +
|
", periodState='" + periodState + '\'' + "\n" +
|
||||||
", payoutState='" + payoutState + '\'' + "\n" +
|
", payoutState='" + payoutState + '\'' + "\n" +
|
||||||
", disputeState='" + disputeState + '\'' + "\n" +
|
", disputeState='" + disputeState + '\'' + "\n" +
|
||||||
", isDepositPublished=" + isDepositPublished + "\n" +
|
", isDepositsPublished=" + isDepositsPublished + "\n" +
|
||||||
", isDepositConfirmed=" + isDepositConfirmed + "\n" +
|
", isDepositsConfirmed=" + isDepositsConfirmed + "\n" +
|
||||||
", isDepositUnlocked=" + isDepositUnlocked + "\n" +
|
", isDepositsUnlocked=" + isDepositsUnlocked + "\n" +
|
||||||
", isPaymentSent=" + isPaymentSent + "\n" +
|
", isPaymentSent=" + isPaymentSent + "\n" +
|
||||||
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
||||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||||
|
|
|
@ -55,9 +55,9 @@ public final class TradeInfoV1Builder {
|
||||||
private String periodState;
|
private String periodState;
|
||||||
private String payoutState;
|
private String payoutState;
|
||||||
private String disputeState;
|
private String disputeState;
|
||||||
private boolean isDepositPublished;
|
private boolean isDepositsPublished;
|
||||||
private boolean isDepositConfirmed;
|
private boolean isDepositsConfirmed;
|
||||||
private boolean isDepositUnlocked;
|
private boolean isDepositsUnlocked;
|
||||||
private boolean isPaymentSent;
|
private boolean isPaymentSent;
|
||||||
private boolean isPaymentReceived;
|
private boolean isPaymentReceived;
|
||||||
private boolean isPayoutPublished;
|
private boolean isPayoutPublished;
|
||||||
|
@ -183,18 +183,18 @@ public final class TradeInfoV1Builder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsDepositPublished(boolean isDepositPublished) {
|
public TradeInfoV1Builder withIsDepositsPublished(boolean isDepositsPublished) {
|
||||||
this.isDepositPublished = isDepositPublished;
|
this.isDepositsPublished = isDepositsPublished;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsDepositConfirmed(boolean isDepositConfirmed) {
|
public TradeInfoV1Builder withIsDepositsConfirmed(boolean isDepositsConfirmed) {
|
||||||
this.isDepositConfirmed = isDepositConfirmed;
|
this.isDepositsConfirmed = isDepositsConfirmed;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TradeInfoV1Builder withIsDepositUnlocked(boolean isDepositUnlocked) {
|
public TradeInfoV1Builder withIsDepositsUnlocked(boolean isDepositsUnlocked) {
|
||||||
this.isDepositUnlocked = isDepositUnlocked;
|
this.isDepositsUnlocked = isDepositsUnlocked;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -500,7 +500,7 @@ public class HavenoSetup {
|
||||||
revolutAccountsUpdateHandler,
|
revolutAccountsUpdateHandler,
|
||||||
amazonGiftCardAccountsUpdateHandler);
|
amazonGiftCardAccountsUpdateHandler);
|
||||||
|
|
||||||
if (walletsSetup.downloadPercentageProperty().get() == 1) {
|
if (walletsSetup.downloadPercentageProperty().get() == 1) { // TODO: update for XMR
|
||||||
checkForLockedUpFunds();
|
checkForLockedUpFunds();
|
||||||
checkForInvalidMakerFeeTxs();
|
checkForInvalidMakerFeeTxs();
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,11 +91,15 @@ public class Balances {
|
||||||
|
|
||||||
private void updatedBalances() {
|
private void updatedBalances() {
|
||||||
if (!xmrWalletService.isWalletReady()) return;
|
if (!xmrWalletService.isWalletReady()) return;
|
||||||
|
try {
|
||||||
updateAvailableBalance();
|
updateAvailableBalance();
|
||||||
updatePendingBalance();
|
updatePendingBalance();
|
||||||
updateReservedOfferBalance();
|
updateReservedOfferBalance();
|
||||||
updateReservedTradeBalance();
|
updateReservedTradeBalance();
|
||||||
updateReservedBalance();
|
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
|
// 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) {
|
synchronized (txCache) {
|
||||||
|
|
||||||
// fetch txs
|
// fetch txs
|
||||||
|
if (getDaemon() == null) connectionsService.verifyConnection(); // will throw
|
||||||
List<MoneroTx> txs = getDaemon().getTxs(txHashes, true);
|
List<MoneroTx> txs = getDaemon().getTxs(txHashes, true);
|
||||||
|
|
||||||
// store to cache
|
// store to cache
|
||||||
|
@ -549,6 +550,7 @@ public class XmrWalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeInitMainWallet() {
|
private void maybeInitMainWallet() {
|
||||||
|
if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
|
||||||
|
|
||||||
// open or create wallet
|
// open or create wallet
|
||||||
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
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
|
// wallet is not initialized until connected to a daemon
|
||||||
if (wallet != null) {
|
if (wallet != null) {
|
||||||
try {
|
|
||||||
wallet.sync(); // blocking
|
// sync wallet which updates app startup state
|
||||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); // start syncing wallet in background
|
trySyncMainWallet();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionsService.getDaemon() == null) System.out.println("Daemon: null");
|
if (connectionsService.getDaemon() == null) System.out.println("Daemon: null");
|
||||||
else {
|
else {
|
||||||
|
@ -671,12 +668,25 @@ public class XmrWalletService {
|
||||||
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
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) {
|
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||||
log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
||||||
if (wallet == null) maybeInitMainWallet();
|
if (wallet == null) maybeInitMainWallet();
|
||||||
if (wallet != null) {
|
else {
|
||||||
wallet.setDaemonConnection(connection);
|
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
|
// 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.";
|
errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced.";
|
||||||
log.info(errorMessage);
|
log.info(errorMessage);
|
||||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||||
|
|
|
@ -50,6 +50,9 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
|
// verify monero connection
|
||||||
|
model.getXmrWalletService().getConnectionsService().verifyConnection();
|
||||||
|
|
||||||
// create reserve tx
|
// create reserve tx
|
||||||
BigInteger makerFee = HavenoUtils.coinToAtomicUnits(offer.getMakerFee());
|
BigInteger makerFee = HavenoUtils.coinToAtomicUnits(offer.getMakerFee());
|
||||||
BigInteger sendAmount = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? Coin.ZERO : offer.getAmount());
|
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.CoreMoneroConnectionsService;
|
||||||
import bisq.core.api.CoreNotificationService;
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
import bisq.core.support.dispute.Dispute;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.support.messages.SupportMessage;
|
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;
|
||||||
import bisq.core.trade.protocol.TradeProtocol.MailboxMessageComparator;
|
import bisq.core.trade.protocol.TradeProtocol.MailboxMessageComparator;
|
||||||
import bisq.network.p2p.AckMessage;
|
import bisq.network.p2p.AckMessage;
|
||||||
|
@ -51,6 +54,7 @@ import javax.annotation.Nullable;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class SupportManager {
|
public abstract class SupportManager {
|
||||||
protected final P2PService p2PService;
|
protected final P2PService p2PService;
|
||||||
|
protected final TradeManager tradeManager;
|
||||||
protected final CoreMoneroConnectionsService connectionService;
|
protected final CoreMoneroConnectionsService connectionService;
|
||||||
protected final CoreNotificationService notificationService;
|
protected final CoreNotificationService notificationService;
|
||||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||||
|
@ -65,11 +69,15 @@ public abstract class SupportManager {
|
||||||
// Constructor
|
// Constructor
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, CoreNotificationService notificationService) {
|
public SupportManager(P2PService p2PService,
|
||||||
|
CoreMoneroConnectionsService connectionService,
|
||||||
|
CoreNotificationService notificationService,
|
||||||
|
TradeManager tradeManager) {
|
||||||
this.p2PService = p2PService;
|
this.p2PService = p2PService;
|
||||||
this.connectionService = connectionService;
|
this.connectionService = connectionService;
|
||||||
this.mailboxMessageService = p2PService.getMailboxMessageService();
|
this.mailboxMessageService = p2PService.getMailboxMessageService();
|
||||||
this.notificationService = notificationService;
|
this.notificationService = notificationService;
|
||||||
|
this.tradeManager = tradeManager;
|
||||||
|
|
||||||
// We get first the message handler called then the onBootstrapped
|
// We get first the message handler called then the onBootstrapped
|
||||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||||
|
@ -181,6 +189,18 @@ public abstract class SupportManager {
|
||||||
if (ackMessage.isSuccess()) {
|
if (ackMessage.isSuccess()) {
|
||||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
||||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
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 {
|
} else {
|
||||||
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
|
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
|
||||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
||||||
|
|
|
@ -47,7 +47,6 @@ import bisq.network.p2p.BootstrapListener;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
import bisq.network.p2p.SendMailboxMessageListener;
|
import bisq.network.p2p.SendMailboxMessageListener;
|
||||||
|
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
import bisq.common.config.Config;
|
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 {
|
public abstract class DisputeManager<T extends DisputeList<Dispute>> extends SupportManager {
|
||||||
protected final TradeWalletService tradeWalletService;
|
protected final TradeWalletService tradeWalletService;
|
||||||
protected final XmrWalletService xmrWalletService;
|
protected final XmrWalletService xmrWalletService;
|
||||||
protected final TradeManager tradeManager;
|
|
||||||
protected final ClosedTradableManager closedTradableManager;
|
protected final ClosedTradableManager closedTradableManager;
|
||||||
protected final OpenOfferManager openOfferManager;
|
protected final OpenOfferManager openOfferManager;
|
||||||
protected final KeyRing keyRing;
|
protected final KeyRing keyRing;
|
||||||
|
@ -122,11 +120,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
DisputeListService<T> disputeListService,
|
DisputeListService<T> disputeListService,
|
||||||
Config config,
|
Config config,
|
||||||
PriceFeedService priceFeedService) {
|
PriceFeedService priceFeedService) {
|
||||||
super(p2PService, connectionService, notificationService);
|
super(p2PService, connectionService, notificationService, tradeManager);
|
||||||
|
|
||||||
this.tradeWalletService = tradeWalletService;
|
this.tradeWalletService = tradeWalletService;
|
||||||
this.xmrWalletService = xmrWalletService;
|
this.xmrWalletService = xmrWalletService;
|
||||||
this.tradeManager = tradeManager;
|
|
||||||
this.closedTradableManager = closedTradableManager;
|
this.closedTradableManager = closedTradableManager;
|
||||||
this.openOfferManager = openOfferManager;
|
this.openOfferManager = openOfferManager;
|
||||||
this.keyRing = keyRing;
|
this.keyRing = keyRing;
|
||||||
|
@ -234,8 +231,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
}
|
}
|
||||||
|
|
||||||
protected T getDisputeList() {
|
protected T getDisputeList() {
|
||||||
|
synchronized(disputeListService.getDisputeList()) {
|
||||||
return disputeListService.getDisputeList();
|
return disputeListService.getDisputeList();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Set<String> getDisputedTradeIds() {
|
public Set<String> getDisputedTradeIds() {
|
||||||
return disputeListService.getDisputedTradeIds();
|
return disputeListService.getDisputedTradeIds();
|
||||||
|
@ -367,7 +366,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
getSupportType(),
|
getSupportType(),
|
||||||
updatedMultisigHex,
|
updatedMultisigHex,
|
||||||
trade.getBuyer().getPaymentSentMessage());
|
trade.getProcessModel().getPaymentSentMessage());
|
||||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress,
|
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
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setArrived(true);
|
chatMessage.setArrived(true);
|
||||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
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
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setStoredInMailbox(true);
|
chatMessage.setStoredInMailbox(true);
|
||||||
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED);
|
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
@ -441,6 +440,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
Dispute dispute = message.getDispute();
|
Dispute dispute = message.getDispute();
|
||||||
log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
log.info("{}.onDisputeOpenedMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||||
|
|
||||||
|
Trade trade = null;
|
||||||
|
String errorMessage = null;
|
||||||
|
PubKeyRing senderPubKeyRing = null;
|
||||||
|
try {
|
||||||
|
|
||||||
// intialize
|
// intialize
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
|
@ -448,7 +452,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispute.setSupportType(message.getSupportType());
|
dispute.setSupportType(message.getSupportType());
|
||||||
dispute.setState(Dispute.State.NEW); // TODO: unused, remove?
|
dispute.setState(Dispute.State.NEW);
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
|
|
||||||
// validate dispute
|
// validate dispute
|
||||||
|
@ -461,19 +465,19 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
} catch (TradeDataValidation.AddressException |
|
} catch (TradeDataValidation.AddressException |
|
||||||
TradeDataValidation.NodeAddressException |
|
TradeDataValidation.NodeAddressException |
|
||||||
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
||||||
log.error(e.toString());
|
|
||||||
validationExceptions.add(e);
|
validationExceptions.add(e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get trade
|
// get trade
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get sender
|
// get sender
|
||||||
PubKeyRing senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||||
TradingPeer sender = trade.getTradingPeer(senderPubKeyRing);
|
TradingPeer sender = trade.getTradingPeer(senderPubKeyRing);
|
||||||
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||||
|
|
||||||
|
@ -500,7 +504,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
// add dispute
|
// add dispute
|
||||||
String errorMessage = null;
|
|
||||||
synchronized (disputeList) {
|
synchronized (disputeList) {
|
||||||
if (!disputeList.contains(dispute)) {
|
if (!disputeList.contains(dispute)) {
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
@ -517,11 +520,18 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||||
dispute.getTradeId());
|
dispute.getTradeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add chat message with mediation info if applicable
|
||||||
|
addMediationResultMessage(dispute);
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
|
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId());
|
||||||
log.warn(errorMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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
|
// use chat message instead of open dispute message for the ack
|
||||||
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
||||||
|
@ -530,9 +540,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add chat message with mediation info if applicable // TODO: not applicable in haveno
|
|
||||||
addMediationResultMessage(dispute);
|
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -635,7 +642,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
getSupportType(),
|
getSupportType(),
|
||||||
updatedMultisigHex,
|
updatedMultisigHex,
|
||||||
trade.getSelf().getPaymentSentMessage());
|
trade.getProcessModel().getPaymentSentMessage());
|
||||||
|
|
||||||
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
|
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}",
|
||||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||||
|
|
|
@ -71,7 +71,7 @@ public class DisputeSummaryVerification {
|
||||||
String fullAddress = textToSign.split("\n")[1].split(": ")[1];
|
String fullAddress = textToSign.split("\n")[1].split(": ")[1];
|
||||||
NodeAddress nodeAddress = new NodeAddress(fullAddress);
|
NodeAddress nodeAddress = new NodeAddress(fullAddress);
|
||||||
DisputeAgent disputeAgent = arbitratorMediator.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
|
DisputeAgent disputeAgent = arbitratorMediator.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
|
||||||
checkNotNull(disputeAgent);
|
checkNotNull(disputeAgent, "Dispute agent is null");
|
||||||
PublicKey pubKey = disputeAgent.getPubKeyRing().getSignaturePubKey();
|
PublicKey pubKey = disputeAgent.getPubKeyRing().getSignaturePubKey();
|
||||||
|
|
||||||
String sigString = parts[1].split(SEPARATOR2)[0];
|
String sigString = parts[1].split(SEPARATOR2)[0];
|
||||||
|
|
|
@ -56,8 +56,12 @@ import com.google.inject.Singleton;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -77,6 +81,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
|
|
||||||
private final ArbitratorManager arbitratorManager;
|
private final ArbitratorManager arbitratorManager;
|
||||||
|
|
||||||
|
private Map<String, Integer> reprocessDisputeClosedMessageCounts = new HashMap<>();
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -117,6 +123,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
log.info("Received {} from {} with tradeId {} and uid {}",
|
log.info("Received {} from {} with tradeId {} and uid {}",
|
||||||
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
if (message instanceof DisputeOpenedMessage) {
|
if (message instanceof DisputeOpenedMessage) {
|
||||||
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
|
||||||
} else if (message instanceof ChatMessage) {
|
} else if (message instanceof ChatMessage) {
|
||||||
|
@ -126,6 +133,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
} else {
|
} else {
|
||||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||||
}
|
}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,24 +181,38 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
// received by both peers when arbitrator closes disputes
|
// received by both peers when arbitrator closes disputes
|
||||||
@Override
|
@Override
|
||||||
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
|
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
|
||||||
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
handleDisputeClosedMessage(disputeClosedMessage, true);
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
}
|
||||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
|
||||||
String tradeId = disputeResult.getTradeId();
|
|
||||||
|
|
||||||
// get trade
|
private void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage, boolean reprocessOnError) {
|
||||||
Trade trade = tradeManager.getTrade(tradeId);
|
|
||||||
|
// get dispute's trade
|
||||||
|
final Trade trade = tradeManager.getTrade(disputeClosedMessage.getTradeId());
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
log.warn("Dispute trade {} does not exist", tradeId);
|
log.warn("Dispute trade {} does not exist", disputeClosedMessage.getTradeId());
|
||||||
return;
|
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());
|
log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId());
|
||||||
|
|
||||||
// verify arbitrator signature
|
// verify arbitrator signature
|
||||||
String summaryText = chatMessage.getMessage();
|
String summaryText = chatMessage.getMessage();
|
||||||
DisputeSummaryVerification.verifySignature(summaryText, arbitratorManager);
|
DisputeSummaryVerification.verifySignature(summaryText, arbitratorManager);
|
||||||
|
|
||||||
|
// save dispute closed message for reprocessing
|
||||||
|
trade.getProcessModel().setDisputeClosedMessage(disputeClosedMessage);
|
||||||
|
requestPersistence();
|
||||||
|
|
||||||
// get dispute
|
// get dispute
|
||||||
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
||||||
String uid = disputeClosedMessage.getUid();
|
String uid = disputeClosedMessage.getUid();
|
||||||
|
@ -208,11 +230,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Dispute dispute = disputeOptional.get();
|
dispute = disputeOptional.get();
|
||||||
|
|
||||||
// verify that arbitrator does not get DisputeClosedMessage
|
// verify that arbitrator does not get DisputeClosedMessage
|
||||||
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
||||||
log.error("Arbitrator received disputeResultMessage. That should never happen.");
|
log.error("Arbitrator received disputeResultMessage. That should never happen.");
|
||||||
|
trade.getProcessModel().setDisputeClosedMessage(null); // don't reprocess
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +252,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
}
|
}
|
||||||
dispute.setDisputeResult(disputeResult);
|
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
|
// import multisig hex
|
||||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().getUpdatedMultisigHex());
|
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getTradingPeer().getUpdatedMultisigHex());
|
||||||
|
@ -239,26 +268,18 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
trade.syncWallet();
|
trade.syncWallet();
|
||||||
trade.saveWallet();
|
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
|
// wait to sign and publish payout tx if defer flag set
|
||||||
if (disputeClosedMessage.isDeferPublishPayout()) {
|
if (disputeClosedMessage.isDeferPublishPayout()) {
|
||||||
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||||
trade.syncWallet();
|
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign and publish dispute payout tx if peer still has not published
|
// sign and publish dispute payout tx if peer still has not published
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
try {
|
try {
|
||||||
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
signAndPublishDisputePayoutTx(trade, disputeClosedMessage.getUnsignedPayoutTxHex());
|
signAndPublishDisputePayoutTx(trade);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
// check if payout published again
|
// check if payout published again
|
||||||
|
@ -266,10 +287,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
if (trade.isPayoutPublished()) {
|
if (trade.isPayoutPublished()) {
|
||||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
} else {
|
} else {
|
||||||
e.printStackTrace();
|
throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId);
|
||||||
errorMessage = "Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId;
|
|
||||||
log.warn(errorMessage);
|
|
||||||
success = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -282,12 +300,47 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
|
|
||||||
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
|
// 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.
|
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}).start();
|
} 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade, String payoutTxHex) {
|
// 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
|
// gather trade info
|
||||||
MoneroWallet multisigWallet = trade.getWallet();
|
MoneroWallet multisigWallet = trade.getWallet();
|
||||||
|
@ -296,6 +349,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
Dispute dispute = disputeOptional.get();
|
Dispute dispute = disputeOptional.get();
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
||||||
|
String unsignedPayoutTxHex = trade.getProcessModel().getDisputeClosedMessage().getUnsignedPayoutTxHex();
|
||||||
|
|
||||||
// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
|
// 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
|
// 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);
|
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
||||||
|
|
||||||
// parse arbitrator-signed payout tx
|
// parse arbitrator-signed payout tx
|
||||||
MoneroTxSet signedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
MoneroTxSet disputeTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(unsignedPayoutTxHex));
|
||||||
if (signedTxSet.getTxs() == null || signedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
if (disputeTxSet.getTxs() == null || disputeTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
||||||
MoneroTxWallet arbitratorSignedPayoutTx = signedTxSet.getTxs().get(0);
|
MoneroTxWallet arbitratorSignedPayoutTx = disputeTxSet.getTxs().get(0);
|
||||||
|
|
||||||
// verify payout tx has 1 or 2 destinations
|
// verify payout tx has 1 or 2 destinations
|
||||||
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
||||||
|
@ -353,11 +407,27 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
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);
|
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
||||||
|
|
||||||
|
// check wallet's daemon connection
|
||||||
|
trade.checkWalletConnection();
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
boolean signed = trade.getPayoutTxHex() != null && !nonSignedDisputePayoutTxHexes.contains(trade.getPayoutTxHex());
|
||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
// sign arbitrator-signed payout tx
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
if (!signed) {
|
||||||
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
signedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
|
trade.setPayoutTxHex(signedMultisigTxHex);
|
||||||
|
requestPersistence();
|
||||||
|
|
||||||
// verify mining fee is within tolerance by recreating payout tx
|
// 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?
|
// 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?
|
||||||
|
@ -370,19 +440,22 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
if (feeEstimateTx != null) {
|
if (feeEstimateTx != null) {
|
||||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||||
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
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());
|
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);
|
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
|
// submit fully signed payout tx to the network
|
||||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(signedTxSet.getMultisigTxHex());
|
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
||||||
signedTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
trade.setPayoutTx(signedTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||||
trade.setPayoutTxId(signedTxSet.getTxs().get(0).getHash());
|
trade.setPayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
||||||
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
||||||
dispute.setDisputePayoutTxId(signedTxSet.getTxs().get(0).getHash());
|
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
||||||
return signedTxSet;
|
return disputeTxSet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public class TraderChatManager extends SupportManager {
|
public class TraderChatManager extends SupportManager {
|
||||||
private final TradeManager tradeManager;
|
|
||||||
private final PubKeyRingProvider pubKeyRingProvider;
|
private final PubKeyRingProvider pubKeyRingProvider;
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,8 +60,7 @@ public class TraderChatManager extends SupportManager {
|
||||||
CoreNotificationService notificationService,
|
CoreNotificationService notificationService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
PubKeyRingProvider pubKeyRingProvider) {
|
PubKeyRingProvider pubKeyRingProvider) {
|
||||||
super(p2PService, connectionService, notificationService);
|
super(p2PService, connectionService, notificationService, tradeManager);
|
||||||
this.tradeManager = tradeManager;
|
|
||||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -294,13 +294,13 @@ public class HavenoUtils {
|
||||||
// verify signature
|
// verify signature
|
||||||
String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
||||||
try {
|
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) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(errMessage);
|
throw new IllegalArgumentException(errMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify trade id
|
// 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
|
// verify signature
|
||||||
String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
|
||||||
try {
|
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) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(errMessage);
|
throw new IllegalArgumentException(errMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify trade id
|
// 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
|
// verify buyer signature of payment sent message
|
||||||
verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
verifyPaymentSentMessage(trade, message.getPaymentSentMessage());
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package bisq.core.trade;
|
package bisq.core.trade;
|
||||||
|
|
||||||
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
import bisq.core.btc.model.XmrAddressEntry;
|
import bisq.core.btc.model.XmrAddressEntry;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.locale.CurrencyUtil;
|
import bisq.core.locale.CurrencyUtil;
|
||||||
|
@ -240,7 +241,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
public enum DisputeState {
|
public enum DisputeState {
|
||||||
NO_DISPUTE,
|
NO_DISPUTE,
|
||||||
DISPUTE_REQUESTED, // TODO: not currently used; can use by subscribing to chat message ack in DisputeManager
|
DISPUTE_REQUESTED,
|
||||||
DISPUTE_OPENED,
|
DISPUTE_OPENED,
|
||||||
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG,
|
ARBITRATOR_SENT_DISPUTE_CLOSED_MSG,
|
||||||
ARBITRATOR_SEND_FAILED_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();
|
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() {
|
public boolean isClosed() {
|
||||||
return this == DisputeState.DISPUTE_CLOSED;
|
return this == DisputeState.DISPUTE_CLOSED;
|
||||||
}
|
}
|
||||||
|
@ -404,6 +413,9 @@ public abstract class Trade implements Tradable, Model {
|
||||||
@Setter
|
@Setter
|
||||||
private long lockTime;
|
private long lockTime;
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private long startTime; // added for haveno
|
||||||
|
@Getter
|
||||||
@Nullable
|
@Nullable
|
||||||
private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT;
|
private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT;
|
||||||
transient final private ObjectProperty<RefundResultState> refundResultStateProperty = new SimpleObjectProperty<>(refundResultState);
|
transient final private ObjectProperty<RefundResultState> refundResultStateProperty = new SimpleObjectProperty<>(refundResultState);
|
||||||
|
@ -444,8 +456,8 @@ public abstract class Trade implements Tradable, Model {
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private String payoutTxKey;
|
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
|
// Constructors
|
||||||
|
@ -588,7 +600,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
// handle trade state events
|
// handle trade state events
|
||||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||||
if (!isInitialized) return;
|
if (!isInitialized) return;
|
||||||
if (isDepositPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
||||||
if (isCompleted()) {
|
if (isCompleted()) {
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
if (tradePhaseSubscription != null) {
|
if (tradePhaseSubscription != null) {
|
||||||
|
@ -648,6 +660,10 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void requestPersistence() {
|
||||||
|
processModel.getTradeManager().requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
public TradeProtocol getProtocol() {
|
public TradeProtocol getProtocol() {
|
||||||
return processModel.getTradeManager().getTradeProtocol(this);
|
return processModel.getTradeManager().getTradeProtocol(this);
|
||||||
}
|
}
|
||||||
|
@ -664,6 +680,22 @@ public abstract class Trade implements Tradable, Model {
|
||||||
return getArbitrator() == null ? null : getArbitrator().getNodeAddress();
|
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.
|
* 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 buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
|
||||||
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
|
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
|
||||||
|
|
||||||
|
// check connection to monero daemon
|
||||||
|
checkWalletConnection();
|
||||||
|
|
||||||
// create transaction to get fee estimate
|
// create transaction to get fee estimate
|
||||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
|
@ -760,20 +795,19 @@ public abstract class Trade implements Tradable, Model {
|
||||||
log.info("Verifying payout tx");
|
log.info("Verifying payout tx");
|
||||||
|
|
||||||
// gather relevant info
|
// gather relevant info
|
||||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
MoneroWallet wallet = getWallet();
|
||||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(getId());
|
|
||||||
Contract contract = getContract();
|
Contract contract = getContract();
|
||||||
BigInteger sellerDepositAmount = multisigWallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
|
BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
|
||||||
BigInteger buyerDepositAmount = multisigWallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
|
BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
|
||||||
BigInteger tradeAmount = HavenoUtils.coinToAtomicUnits(getAmount());
|
BigInteger tradeAmount = HavenoUtils.coinToAtomicUnits(getAmount());
|
||||||
|
|
||||||
// describe payout tx
|
// describe payout tx
|
||||||
MoneroTxSet describedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||||
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad payout tx"); // TODO (woodser): test nack
|
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
|
||||||
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
|
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
|
||||||
|
|
||||||
// verify payout tx has exactly 2 destinations
|
// 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)
|
// get buyer and seller destinations (order not preserved)
|
||||||
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
|
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);
|
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
|
||||||
|
|
||||||
// verify payout addresses
|
// verify payout addresses
|
||||||
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer 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 RuntimeException("Seller 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
|
// 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
|
// 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
|
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
|
||||||
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
|
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
|
||||||
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
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
|
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
|
||||||
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
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
|
// handle tx signing
|
||||||
if (sign) {
|
if (sign) {
|
||||||
|
|
||||||
// sign tx
|
// sign tx
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
||||||
payoutTxHex = result.getSignedMultisigTxHex();
|
payoutTxHex = result.getSignedMultisigTxHex();
|
||||||
describedTxSet = multisigWallet.describeMultisigTxSet(payoutTxHex); // update described set
|
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); // update described set
|
||||||
payoutTx = describedTxSet.getTxs().get(0);
|
payoutTx = describedTxSet.getTxs().get(0);
|
||||||
|
|
||||||
// verify fee is within tolerance by recreating payout tx
|
// verify fee is within tolerance by recreating payout tx
|
||||||
|
@ -820,7 +857,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (feeEstimateTx != null) {
|
if (feeEstimateTx != null) {
|
||||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
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);
|
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
|
// submit payout tx
|
||||||
if (publish) {
|
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();
|
pollWallet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -926,14 +964,8 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncWallet() {
|
public void syncWallet() {
|
||||||
if (getWallet() == null) {
|
if (getWallet() == null) throw new RuntimeException("Cannot sync multisig wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
||||||
log.warn("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());
|
||||||
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;
|
|
||||||
}
|
|
||||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
getWallet().sync();
|
getWallet().sync();
|
||||||
pollWallet();
|
pollWallet();
|
||||||
|
@ -941,6 +973,14 @@ public abstract class Trade implements Tradable, Model {
|
||||||
updateWalletRefreshPeriod();
|
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) {
|
public void syncWalletNormallyForMs(long syncNormalDuration) {
|
||||||
syncNormalStartTime = System.currentTimeMillis();
|
syncNormalStartTime = System.currentTimeMillis();
|
||||||
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
||||||
|
@ -957,7 +997,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (xmrWalletService.multisigWalletExists(getId())) {
|
if (xmrWalletService.multisigWalletExists(getId())) {
|
||||||
|
|
||||||
// delete trade wallet unless funded
|
// 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());
|
log.warn("Refusing to delete wallet for {} {} because it could be funded", getClass().getSimpleName(), getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1258,20 +1298,29 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getStartTime() {
|
private long getStartTime() {
|
||||||
if (startTime != null) return startTime;
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (isDepositConfirmed() && getTakeOfferDate() != null) {
|
if (isDepositsConfirmed() && getTakeOfferDate() != null) {
|
||||||
if (isDepositUnlocked()) {
|
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());
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStartTimeFromUnlockedTxs() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
final long tradeTime = getTakeOfferDate().getTime();
|
final long tradeTime = getTakeOfferDate().getTime();
|
||||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
|
||||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
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();
|
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||||
|
|
||||||
// if (depositTx.getConfidence().getDepthInBlocks() > 0) {
|
// If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date.
|
||||||
// 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 block date is earlier than our trade date we use our trade date.
|
||||||
if (blockTime > now)
|
if (blockTime > now)
|
||||||
startTime = now;
|
startTime = now;
|
||||||
|
@ -1280,14 +1329,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
|
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));
|
new Date(startTime), new Date(tradeTime), new Date(blockTime));
|
||||||
} else {
|
|
||||||
log.debug("depositTx not confirmed yet. We don't start counting remaining trade period yet. makerTxId={}, takerTxId={}", getMaker().getDepositTxHash(), getTaker().getDepositTxHash());
|
|
||||||
startTime = now;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startTime = now;
|
|
||||||
}
|
|
||||||
return startTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasFailed() {
|
public boolean hasFailed() {
|
||||||
|
@ -1306,19 +1347,19 @@ public abstract class Trade implements Tradable, Model {
|
||||||
return getState() == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED;
|
return getState() == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositPublished() {
|
public boolean isDepositsPublished() {
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal();
|
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFundsLockedIn() {
|
public boolean isFundsLockedIn() {
|
||||||
return isDepositPublished() && !isPayoutPublished();
|
return isDepositsPublished() && !isPayoutPublished();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositConfirmed() {
|
public boolean isDepositsConfirmed() {
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
return getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositUnlocked() {
|
public boolean isDepositsUnlocked() {
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1458,6 +1499,19 @@ public abstract class Trade implements Tradable, Model {
|
||||||
processModel.getTaker().getDepositTxHash() == null;
|
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
|
// Private
|
||||||
|
@ -1479,18 +1533,27 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||||
if (getWallet() == null) return;
|
MoneroWallet wallet = getWallet();
|
||||||
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
|
if (wallet == null) return;
|
||||||
if (getWallet() != null) getWallet().setDaemonConnection(connection);
|
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();
|
updateSyncing();
|
||||||
|
|
||||||
|
// reprocess pending payout messages
|
||||||
|
this.getProtocol().maybeReprocessPaymentReceivedMessage(false);
|
||||||
|
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSyncing() {
|
private void updateSyncing() {
|
||||||
if (!isIdling()) syncWallet();
|
if (!isIdling()) trySyncWallet();
|
||||||
else {
|
else {
|
||||||
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
if (isInitialized) syncWallet();
|
if (isInitialized) trySyncWallet();
|
||||||
}, startSyncingInMs / 1000l);
|
}, startSyncingInMs / 1000l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1525,7 +1588,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (isPayoutUnlocked()) return;
|
if (isPayoutUnlocked()) return;
|
||||||
|
|
||||||
// rescan spent if deposits unlocked
|
// rescan spent if deposits unlocked
|
||||||
if (isDepositUnlocked()) getWallet().rescanSpent();
|
if (isDepositsUnlocked()) getWallet().rescanSpent();
|
||||||
|
|
||||||
// get txs with outputs
|
// get txs with outputs
|
||||||
List<MoneroTxWallet> txs;
|
List<MoneroTxWallet> txs;
|
||||||
|
@ -1538,7 +1601,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check deposit txs
|
// check deposit txs
|
||||||
if (!isDepositUnlocked()) {
|
if (!isDepositsUnlocked()) {
|
||||||
if (txs.size() == 2) {
|
if (txs.size() == 2) {
|
||||||
setStateDepositsPublished();
|
setStateDepositsPublished();
|
||||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||||
|
@ -1585,19 +1648,22 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isIdling() {
|
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() {
|
private void setStateDepositsPublished() {
|
||||||
if (!isDepositPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
|
if (!isDepositsPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStateDepositsConfirmed() {
|
private void setStateDepositsConfirmed() {
|
||||||
if (!isDepositConfirmed()) setState(State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN);
|
if (!isDepositsConfirmed()) setState(State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStateDepositsUnlocked() {
|
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() {
|
private void setPayoutStatePublished() {
|
||||||
|
@ -1634,6 +1700,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
||||||
.collect(Collectors.toList()))
|
.collect(Collectors.toList()))
|
||||||
.setLockTime(lockTime)
|
.setLockTime(lockTime)
|
||||||
|
.setStartTime(startTime)
|
||||||
.setUid(uid);
|
.setUid(uid);
|
||||||
|
|
||||||
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
|
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
|
||||||
|
@ -1668,6 +1735,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
|
trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState()));
|
||||||
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
|
trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState()));
|
||||||
trade.setLockTime(proto.getLockTime());
|
trade.setLockTime(proto.getLockTime());
|
||||||
|
trade.setStartTime(proto.getStartTime());
|
||||||
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
|
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
|
||||||
|
|
||||||
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
|
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
|
||||||
|
@ -1722,6 +1790,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
",\n mediationResultState=" + mediationResultState +
|
",\n mediationResultState=" + mediationResultState +
|
||||||
",\n mediationResultStateProperty=" + mediationResultStateProperty +
|
",\n mediationResultStateProperty=" + mediationResultStateProperty +
|
||||||
",\n lockTime=" + lockTime +
|
",\n lockTime=" + lockTime +
|
||||||
|
",\n startTime=" + startTime +
|
||||||
",\n refundResultState=" + refundResultState +
|
",\n refundResultState=" + refundResultState +
|
||||||
",\n refundResultStateProperty=" + refundResultStateProperty +
|
",\n refundResultStateProperty=" + refundResultStateProperty +
|
||||||
"\n}";
|
"\n}";
|
||||||
|
|
|
@ -369,6 +369,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// notify that persisted trades initialized
|
||||||
persistedTradesInitialized.set(true);
|
persistedTradesInitialized.set(true);
|
||||||
|
|
||||||
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
// 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) {
|
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());
|
log.warn("Scheduling to delete trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
|
|
|
@ -23,10 +23,8 @@ import bisq.network.p2p.NodeAddress;
|
||||||
|
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.proto.network.NetworkEnvelope;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -124,7 +122,7 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||||
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
|
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
|
// 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.
|
// are empty/null, so we check for the signature to see if we got a valid buyerSignedWitness.
|
||||||
protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getBuyerAccountAgeWitness();
|
protobuf.AccountAgeWitness protoAccountAgeWitness = proto.getBuyerAccountAgeWitness();
|
||||||
|
|
|
@ -20,14 +20,12 @@ package bisq.core.trade.messages;
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
|
|
||||||
import bisq.network.p2p.DirectMessage;
|
import bisq.network.p2p.DirectMessage;
|
||||||
import bisq.network.p2p.NodeAddress;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.util.Utilities;
|
import bisq.common.util.Utilities;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||||
|
|
||||||
// re-send payment sent message if not arrived
|
// re-send payment sent message if not arrived
|
||||||
synchronized (trade) {
|
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();
|
latchTrade();
|
||||||
given(anyPhase(Trade.Phase.PAYMENT_SENT)
|
given(anyPhase(Trade.Phase.PAYMENT_SENT)
|
||||||
.with(BuyerEvent.STARTUP))
|
.with(BuyerEvent.STARTUP))
|
||||||
|
|
|
@ -31,10 +31,13 @@ import bisq.core.payment.payload.PaymentAccountPayload;
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
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.support.dispute.refund.refundagent.RefundAgentManager;
|
||||||
import bisq.core.trade.MakerTrade;
|
import bisq.core.trade.MakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.TradeManager;
|
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.messages.TradeMessage;
|
||||||
import bisq.core.trade.statistics.ReferralIdService;
|
import bisq.core.trade.statistics.ReferralIdService;
|
||||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||||
|
@ -43,7 +46,7 @@ import bisq.core.user.User;
|
||||||
import bisq.network.p2p.AckMessage;
|
import bisq.network.p2p.AckMessage;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
|
import bisq.common.app.Version;
|
||||||
import bisq.common.crypto.KeyRing;
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
|
@ -175,6 +178,18 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private boolean isDepositsConfirmedMessagesDelivered;
|
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
|
// 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.
|
// 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(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage()));
|
||||||
Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(makerSignature));
|
Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(makerSignature));
|
||||||
Optional.ofNullable(multisigAddress).ifPresent(e -> builder.setMultisigAddress(multisigAddress));
|
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();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,6 +285,9 @@ public class ProcessModel implements Model, PersistablePayload {
|
||||||
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
||||||
processModel.setPaymentStartedMessageState(paymentStartedMessageState);
|
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;
|
return processModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ public class SellerProtocol extends DisputeProtocol {
|
||||||
|
|
||||||
// re-send payment received message if not arrived
|
// re-send payment received message if not arrived
|
||||||
synchronized (trade) {
|
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();
|
latchTrade();
|
||||||
given(anyPhase(Trade.Phase.PAYMENT_RECEIVED)
|
given(anyPhase(Trade.Phase.PAYMENT_RECEIVED)
|
||||||
.with(SellerEvent.STARTUP))
|
.with(SellerEvent.STARTUP))
|
||||||
|
|
|
@ -74,7 +74,6 @@ import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.fxmisc.easybind.EasyBind;
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
@ -94,6 +93,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
protected TradeResultHandler tradeResultHandler;
|
protected TradeResultHandler tradeResultHandler;
|
||||||
protected ErrorMessageHandler errorMessageHandler;
|
protected ErrorMessageHandler errorMessageHandler;
|
||||||
|
|
||||||
|
private int reprocessPaymentReceivedMessageCount;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// 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) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
|
@ -462,17 +479,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
|
|
||||||
// received by buyer and arbitrator
|
// received by buyer and arbitrator
|
||||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
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)");
|
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
|
||||||
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {
|
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {
|
||||||
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (trade instanceof ArbitratorTrade && !trade.isPayoutUnlocked()) trade.syncWallet(); // arbitrator syncs slowly after deposits confirmed
|
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
processModel.setTradeMessage(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)
|
.with(message)
|
||||||
.from(peer))
|
.from(peer))
|
||||||
.setup(tasks(
|
.setup(tasks(
|
||||||
|
@ -482,7 +505,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
handleTaskRunnerSuccess(peer, message);
|
handleTaskRunnerSuccess(peer, message);
|
||||||
},
|
},
|
||||||
errorMessage -> {
|
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);
|
.executeTasks(true);
|
||||||
awaitTradeLatch();
|
awaitTradeLatch();
|
||||||
|
@ -548,8 +583,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
private void onAckMessage(AckMessage ackMessage, NodeAddress peer) {
|
private void onAckMessage(AckMessage ackMessage, NodeAddress peer) {
|
||||||
// We handle the ack for PaymentSentMessage and DepositTxAndDelayedPayoutTxMessage
|
// 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
|
// 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()) && trade.getTradingPeer(peer) == trade.getSeller()) {
|
||||||
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
|
|
||||||
processModel.setPaymentStartedAckMessage(ackMessage);
|
processModel.setPaymentStartedAckMessage(ackMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,7 @@ import bisq.core.account.witness.AccountAgeWitness;
|
||||||
import bisq.core.btc.model.RawTransactionInput;
|
import bisq.core.btc.model.RawTransactionInput;
|
||||||
import bisq.core.payment.payload.PaymentAccountPayload;
|
import bisq.core.payment.payload.PaymentAccountPayload;
|
||||||
import bisq.core.proto.CoreProtoResolver;
|
import bisq.core.proto.CoreProtoResolver;
|
||||||
import bisq.core.trade.messages.PaymentSentMessage;
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.common.app.Version;
|
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.proto.persistable.PersistablePayload;
|
import bisq.common.proto.persistable.PersistablePayload;
|
||||||
|
@ -131,8 +129,6 @@ public final class TradingPeer implements PersistablePayload {
|
||||||
private String depositTxKey;
|
private String depositTxKey;
|
||||||
@Nullable
|
@Nullable
|
||||||
private String updatedMultisigHex;
|
private String updatedMultisigHex;
|
||||||
@Nullable
|
|
||||||
private PaymentSentMessage paymentSentMessage;
|
|
||||||
|
|
||||||
public TradingPeer() {
|
public TradingPeer() {
|
||||||
}
|
}
|
||||||
|
@ -173,7 +169,6 @@ public final class TradingPeer implements PersistablePayload {
|
||||||
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
|
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
|
||||||
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
|
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
|
||||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||||
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
|
|
||||||
|
|
||||||
builder.setCurrentDate(currentDate);
|
builder.setCurrentDate(currentDate);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
@ -224,7 +219,6 @@ public final class TradingPeer implements PersistablePayload {
|
||||||
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
|
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
|
||||||
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
|
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
|
||||||
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
||||||
tradingPeer.setPaymentSentMessage(proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), Version.getP2PMessageVersion()) : null);
|
|
||||||
return tradingPeer;
|
return tradingPeer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,13 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
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
|
// validate state
|
||||||
Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null");
|
Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null");
|
||||||
Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null");
|
Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null");
|
||||||
|
|
|
@ -73,7 +73,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
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
|
// 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
|
// 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);
|
String messageAsJson = JsonUtil.objectToJson(message);
|
||||||
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
||||||
message.setBuyerSignature(sig);
|
message.setBuyerSignature(sig);
|
||||||
trade.getSelf().setPaymentSentMessage(message);
|
processModel.setPaymentSentMessage(message);
|
||||||
|
trade.requestPersistence();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException (e);
|
throw new RuntimeException (e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return trade.getSelf().getPaymentSentMessage();
|
return processModel.getPaymentSentMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -36,6 +36,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProcessPaymentReceivedMessage extends TradeTask {
|
public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||||
|
@ -46,6 +48,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
protected void run() {
|
protected void run() {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
log.debug("current trade state " + trade.getState());
|
log.debug("current trade state " + trade.getState());
|
||||||
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
|
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
|
||||||
checkNotNull(message);
|
checkNotNull(message);
|
||||||
|
@ -54,6 +57,12 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
|
|
||||||
// verify signature of payment received message
|
// verify signature of payment received message
|
||||||
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
|
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
|
||||||
|
|
||||||
|
// save message for reprocessing
|
||||||
|
processModel.setPaymentReceivedMessage(message);
|
||||||
|
trade.requestPersistence();
|
||||||
|
|
||||||
|
// set state
|
||||||
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
trade.getBuyer().setUpdatedMultisigHex(message.getPaymentSentMessage().getUpdatedMultisigHex());
|
||||||
trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness());
|
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
|
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
|
||||||
|
|
||||||
// close open disputes
|
// 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);
|
trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
for (Dispute dispute : trade.getDisputes()) {
|
for (Dispute dispute : trade.getDisputes()) {
|
||||||
dispute.setIsClosed();
|
dispute.setIsClosed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure connected to monero network
|
||||||
|
trade.checkWalletConnection();
|
||||||
|
|
||||||
// process payout tx unless already unlocked
|
// process payout tx unless already unlocked
|
||||||
if (!trade.isPayoutUnlocked()) processPayoutTx(message);
|
if (!trade.isPayoutUnlocked()) processPayoutTx(message);
|
||||||
|
|
||||||
|
@ -83,25 +95,32 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
|
|
||||||
// complete
|
// complete
|
||||||
trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator auto completes when payout published
|
trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator auto completes when payout published
|
||||||
processModel.getTradeManager().requestPersistence();
|
trade.requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|
||||||
|
// do not reprocess illegal argument
|
||||||
|
if (t instanceof IllegalArgumentException) {
|
||||||
|
processModel.setPaymentReceivedMessage(null); // do not reprocess
|
||||||
|
trade.requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processPayoutTx(PaymentReceivedMessage message) {
|
private void processPayoutTx(PaymentReceivedMessage message) {
|
||||||
|
|
||||||
|
// sync and save wallet
|
||||||
|
trade.syncWallet();
|
||||||
|
trade.saveWallet();
|
||||||
|
|
||||||
// import multisig hex
|
// import multisig hex
|
||||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().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
|
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
|
// handle if payout tx not published
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
|
|
||||||
|
@ -110,18 +129,23 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
|
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
|
||||||
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||||
trade.syncWallet();
|
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify and publish payout tx
|
// verify and publish payout tx
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
if (isSigned) {
|
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);
|
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
||||||
} else {
|
} else {
|
||||||
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
|
|
||||||
try {
|
try {
|
||||||
|
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);
|
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) {
|
} catch (Exception e) {
|
||||||
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
||||||
else throw e;
|
else throw e;
|
||||||
|
|
|
@ -44,10 +44,10 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
||||||
// verify signature of payment sent message
|
// verify signature of payment sent message
|
||||||
HavenoUtils.verifyPaymentSentMessage(trade, message);
|
HavenoUtils.verifyPaymentSentMessage(trade, message);
|
||||||
|
|
||||||
// update buyer info
|
// set state
|
||||||
|
processModel.setPaymentSentMessage(message);
|
||||||
trade.setPayoutTxHex(message.getPayoutTxHex());
|
trade.setPayoutTxHex(message.getPayoutTxHex());
|
||||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
trade.getBuyer().setPaymentSentMessage(message);
|
|
||||||
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
|
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
|
||||||
|
|
||||||
// if seller, decrypt buyer's payment account payload
|
// if seller, decrypt buyer's payment account payload
|
||||||
|
@ -62,7 +62,7 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
||||||
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
||||||
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
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);
|
trade.setStateIfProgress(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||||
processModel.getTradeManager().requestPersistence();
|
trade.requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package bisq.core.trade.protocol.tasks;
|
package bisq.core.trade.protocol.tasks;
|
||||||
|
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -42,6 +41,12 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
|
// check connection
|
||||||
|
trade.checkWalletConnection();
|
||||||
|
|
||||||
|
// handle first time preparation
|
||||||
|
if (processModel.getPaymentReceivedMessage() == null) {
|
||||||
|
|
||||||
// import multisig hex
|
// import multisig hex
|
||||||
MoneroWallet multisigWallet = trade.getWallet();
|
MoneroWallet multisigWallet = trade.getWallet();
|
||||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||||
|
@ -64,6 +69,12 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
||||||
trade.setPayoutTx(payoutTx);
|
trade.setPayoutTx(payoutTx);
|
||||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
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();
|
processModel.getTradeManager().requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
|
|
|
@ -39,8 +39,8 @@ import com.google.common.base.Charsets;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||||
SignedWitness signedWitness = null;
|
|
||||||
PaymentReceivedMessage message = null;
|
PaymentReceivedMessage message = null;
|
||||||
|
SignedWitness signedWitness = null;
|
||||||
|
|
||||||
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||||
super(taskHandler, 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.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
|
||||||
trade.getTradingPeer().getAccountAgeWitness(),
|
trade.getTradingPeer().getAccountAgeWitness(),
|
||||||
signedWitness,
|
signedWitness,
|
||||||
trade.getBuyer().getPaymentSentMessage()
|
processModel.getPaymentSentMessage()
|
||||||
);
|
);
|
||||||
|
|
||||||
// sign message
|
// sign message
|
||||||
|
@ -95,6 +95,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
|
||||||
String messageAsJson = JsonUtil.objectToJson(message);
|
String messageAsJson = JsonUtil.objectToJson(message);
|
||||||
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
byte[] sig = Sig.sign(processModel.getP2PService().getKeyRing().getSignatureKeyPair().getPrivate(), messageAsJson.getBytes(Charsets.UTF_8));
|
||||||
message.setSellerSignature(sig);
|
message.setSellerSignature(sig);
|
||||||
|
processModel.setPaymentReceivedMessage(message);
|
||||||
|
trade.requestPersistence();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
package bisq.core.trade.protocol.tasks;
|
package bisq.core.trade.protocol.tasks;
|
||||||
|
|
||||||
import bisq.core.trade.Trade;
|
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.core.trade.messages.TradeMessage;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
|
@ -34,6 +36,15 @@ public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentRe
|
||||||
super(taskHandler, trade);
|
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() {
|
protected NodeAddress getReceiverNodeAddress() {
|
||||||
return trade.getBuyer().getNodeAddress();
|
return trade.getBuyer().getNodeAddress();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1139,6 +1139,7 @@ support.role=Role
|
||||||
support.agent=Support agent
|
support.agent=Support agent
|
||||||
support.state=State
|
support.state=State
|
||||||
support.chat=Chat
|
support.chat=Chat
|
||||||
|
support.requested=Requested
|
||||||
support.closed=Closed
|
support.closed=Closed
|
||||||
support.open=Open
|
support.open=Open
|
||||||
support.process=Process
|
support.process=Process
|
||||||
|
@ -1967,6 +1968,7 @@ tradeDetailsWindow.txFee=Mining fee
|
||||||
tradeDetailsWindow.tradingPeersOnion=Trading peers onion address
|
tradeDetailsWindow.tradingPeersOnion=Trading peers onion address
|
||||||
tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash
|
tradeDetailsWindow.tradingPeersPubKeyHash=Trading peers pubkey hash
|
||||||
tradeDetailsWindow.tradeState=Trade state
|
tradeDetailsWindow.tradeState=Trade state
|
||||||
|
tradeDetailsWindow.tradePhase=Trade phase
|
||||||
tradeDetailsWindow.agentAddresses=Arbitrator/Mediator
|
tradeDetailsWindow.agentAddresses=Arbitrator/Mediator
|
||||||
tradeDetailsWindow.detailData=Detail data
|
tradeDetailsWindow.detailData=Detail data
|
||||||
|
|
||||||
|
|
|
@ -409,7 +409,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||||
placeOfferHandlerOptional.ifPresent(Runnable::run);
|
placeOfferHandlerOptional.ifPresent(Runnable::run);
|
||||||
} else {
|
} else {
|
||||||
State lastState = Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS;
|
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);
|
takeOfferHandlerOptional.ifPresent(Runnable::run);
|
||||||
|
|
||||||
// update trade state progress
|
// update trade state progress
|
||||||
|
@ -417,7 +417,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
|
||||||
Trade trade = tradeManager.getTrade(offer.getId());
|
Trade trade = tradeManager.getTrade(offer.getId());
|
||||||
if (trade == null) return;
|
if (trade == null) return;
|
||||||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newState -> {
|
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);
|
spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo") + " " + progress);
|
||||||
|
|
||||||
// unsubscribe when done
|
// unsubscribe when done
|
||||||
|
|
|
@ -299,7 +299,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
textArea.scrollTopProperty().addListener(changeListener);
|
textArea.scrollTopProperty().addListener(changeListener);
|
||||||
textArea.setScrollTop(30);
|
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,
|
Tuple3<Button, Button, HBox> tuple = add2ButtonsWithBox(gridPane, ++rowIndex,
|
||||||
|
@ -322,10 +322,13 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
viewContractButton.setOnAction(e -> {
|
viewContractButton.setOnAction(e -> {
|
||||||
TextArea textArea = new HavenoTextArea();
|
TextArea textArea = new HavenoTextArea();
|
||||||
textArea.setText(trade.getContractAsJson());
|
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 += trade.getContractAsJson();
|
||||||
data += "\n\nOther detail data:";
|
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.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();
|
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.ArbitratorTrade;
|
||||||
import bisq.core.trade.BuyerTrade;
|
import bisq.core.trade.BuyerTrade;
|
||||||
import bisq.core.trade.ClosedTradableManager;
|
import bisq.core.trade.ClosedTradableManager;
|
||||||
import bisq.core.trade.Contract;
|
|
||||||
import bisq.core.trade.HavenoUtils;
|
import bisq.core.trade.HavenoUtils;
|
||||||
import bisq.core.trade.SellerTrade;
|
import bisq.core.trade.SellerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
@ -433,21 +432,19 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||||
buyerState.set(BuyerState.STEP2);
|
buyerState.set(BuyerState.STEP2);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// seller step 3
|
// payment received
|
||||||
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
|
|
||||||
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
|
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
|
||||||
if (trade instanceof BuyerTrade) buyerState.set(BuyerState.STEP4);
|
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;
|
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:
|
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;
|
break;
|
||||||
|
|
||||||
case TRADE_COMPLETED:
|
case TRADE_COMPLETED:
|
||||||
|
|
|
@ -801,8 +801,9 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
protected void checkForTimeout() {
|
protected void checkForUnconfirmedTimeout() {
|
||||||
long unconfirmedHours = Duration.between(trade.getTakeOfferDate().toInstant(), Instant.now()).toHours();
|
if (trade.isDepositsConfirmed()) return;
|
||||||
|
long unconfirmedHours = Duration.between(trade.getDate().toInstant(), Instant.now()).toHours();
|
||||||
if (unconfirmedHours >= 3 && !trade.hasFailed()) {
|
if (unconfirmedHours >= 3 && !trade.hasFailed()) {
|
||||||
String key = "tradeUnconfirmedTooLong_" + trade.getShortId();
|
String key = "tradeUnconfirmedTooLong_" + trade.getShortId();
|
||||||
if (DontShowAgainLookup.showAgain(key)) {
|
if (DontShowAgainLookup.showAgain(key)) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class BuyerStep1View extends TradeStepView {
|
||||||
super.onPendingTradesInitialized();
|
super.onPendingTradesInitialized();
|
||||||
//validatePayoutTx(); // TODO (woodser): no payout tx in xmr integration, do something else?
|
//validatePayoutTx(); // TODO (woodser): no payout tx in xmr integration, do something else?
|
||||||
//validateDepositInputs();
|
//validateDepositInputs();
|
||||||
checkForTimeout();
|
checkForUnconfirmedTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package bisq.desktop.main.portfolio.pendingtrades.steps.buyer;
|
package bisq.desktop.main.portfolio.pendingtrades.steps.buyer;
|
||||||
|
|
||||||
import bisq.desktop.components.AutoTooltipButton;
|
|
||||||
import bisq.desktop.components.BusyAnimation;
|
import bisq.desktop.components.BusyAnimation;
|
||||||
import bisq.desktop.components.TextFieldWithCopyIcon;
|
import bisq.desktop.components.TextFieldWithCopyIcon;
|
||||||
import bisq.desktop.components.TitledGroupBg;
|
import bisq.desktop.components.TitledGroupBg;
|
||||||
|
@ -155,7 +154,7 @@ public class BuyerStep2View extends TradeStepView {
|
||||||
if (timeoutTimer != null)
|
if (timeoutTimer != null)
|
||||||
timeoutTimer.stop();
|
timeoutTimer.stop();
|
||||||
|
|
||||||
if (trade.isDepositUnlocked() && !trade.isPaymentSent()) {
|
if (trade.isDepositsUnlocked() && !trade.isPaymentSent()) {
|
||||||
showPopup();
|
showPopup();
|
||||||
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) {
|
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) {
|
||||||
if (!trade.hasFailed()) {
|
if (!trade.hasFailed()) {
|
||||||
|
@ -481,6 +480,10 @@ public class BuyerStep2View extends TradeStepView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!model.dataModel.isReadyForTxBroadcast()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PaymentAccountPayload sellersPaymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
|
PaymentAccountPayload sellersPaymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
|
||||||
Trade trade = checkNotNull(model.dataModel.getTrade(), "trade must not be null");
|
Trade trade = checkNotNull(model.dataModel.getTrade(), "trade must not be null");
|
||||||
if (sellersPaymentAccountPayload instanceof CashDepositAccountPayload) {
|
if (sellersPaymentAccountPayload instanceof CashDepositAccountPayload) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class SellerStep1View extends TradeStepView {
|
||||||
super.onPendingTradesInitialized();
|
super.onPendingTradesInitialized();
|
||||||
//validateDepositInputs();
|
//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?
|
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;
|
HBox hBox = tuple.fourth;
|
||||||
GridPane.setColumnSpan(tuple.fourth, 2);
|
GridPane.setColumnSpan(tuple.fourth, 2);
|
||||||
confirmButton = tuple.first;
|
confirmButton = tuple.first;
|
||||||
|
confirmButton.setDisable(!confirmPaymentReceivedPermitted());
|
||||||
confirmButton.setOnAction(e -> onPaymentReceived());
|
confirmButton.setOnAction(e -> onPaymentReceived());
|
||||||
busyAnimation = tuple.second;
|
busyAnimation = tuple.second;
|
||||||
statusLabel = tuple.third;
|
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
|
// Info
|
||||||
|
@ -357,7 +362,7 @@ public class SellerStep3View extends TradeStepView {
|
||||||
protected void updateDisputeState(Trade.DisputeState disputeState) {
|
protected void updateDisputeState(Trade.DisputeState disputeState) {
|
||||||
super.updateDisputeState(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());
|
log.info("User pressed the [Confirm payment receipt] button for Trade {}", trade.getShortId());
|
||||||
busyAnimation.play();
|
busyAnimation.play();
|
||||||
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
||||||
|
confirmButton.setDisable(true);
|
||||||
|
|
||||||
model.dataModel.onPaymentReceived(() -> {
|
model.dataModel.onPaymentReceived(() -> {
|
||||||
}, errorMessage -> {
|
}, errorMessage -> {
|
||||||
busyAnimation.stop();
|
busyAnimation.stop();
|
||||||
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show();
|
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.Contract;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.TradeManager;
|
import bisq.core.trade.TradeManager;
|
||||||
|
import bisq.core.trade.Trade.DisputeState;
|
||||||
import bisq.core.user.Preferences;
|
import bisq.core.user.Preferences;
|
||||||
import bisq.core.util.FormattingUtils;
|
import bisq.core.util.FormattingUtils;
|
||||||
import bisq.core.util.coin.CoinFormatter;
|
import bisq.core.util.coin.CoinFormatter;
|
||||||
|
@ -1341,18 +1342,21 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||||
|
|
||||||
ReadOnlyBooleanProperty closedProperty;
|
ReadOnlyBooleanProperty closedProperty;
|
||||||
ChangeListener<Boolean> listener;
|
ChangeListener<Boolean> listener;
|
||||||
|
Subscription subscription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(final Dispute item, boolean empty) {
|
public void updateItem(final Dispute item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
if (item != null && !empty) {
|
if (item != null && !empty) {
|
||||||
if (closedProperty != null) {
|
if (closedProperty != null) closedProperty.removeListener(listener);
|
||||||
closedProperty.removeListener(listener);
|
if (subscription != null) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
listener = (observable, oldValue, newValue) -> {
|
listener = (observable, oldValue, newValue) -> {
|
||||||
setText(newValue ? Res.get("support.closed") : Res.get("support.open"));
|
setText(getDisputeStateText(item));
|
||||||
if (getTableRow() != null)
|
if (getTableRow() != null)
|
||||||
getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
||||||
if (item.isClosed() && item == chatPopup.getSelectedDispute())
|
if (item.isClosed() && item == chatPopup.getSelectedDispute())
|
||||||
|
@ -1361,14 +1365,23 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||||
closedProperty = item.isClosedProperty();
|
closedProperty = item.isClosedProperty();
|
||||||
closedProperty.addListener(listener);
|
closedProperty.addListener(listener);
|
||||||
boolean isClosed = item.isClosed();
|
boolean isClosed = item.isClosed();
|
||||||
setText(isClosed ? Res.get("support.closed") : Res.get("support.open"));
|
setText(getDisputeStateText(item));
|
||||||
if (getTableRow() != null)
|
if (getTableRow() != null)
|
||||||
getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
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 {
|
} else {
|
||||||
if (closedProperty != null) {
|
if (closedProperty != null) {
|
||||||
closedProperty.removeListener(listener);
|
closedProperty.removeListener(listener);
|
||||||
closedProperty = null;
|
closedProperty = null;
|
||||||
}
|
}
|
||||||
|
if (subscription != null) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscription = null;
|
||||||
|
}
|
||||||
setText("");
|
setText("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1379,6 +1392,33 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||||
return column;
|
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) {
|
private void openChat(Dispute dispute) {
|
||||||
chatPopup.openChat(dispute, getConcreteDisputeChatSession(dispute), getCounterpartyName());
|
chatPopup.openChat(dispute, getConcreteDisputeChatSession(dispute), getCounterpartyName());
|
||||||
dispute.setDisputeSeen(senderFlag());
|
dispute.setDisputeSeen(senderFlag());
|
||||||
|
|
|
@ -738,11 +738,18 @@ public class GUIUtil {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
connectionService.verifyConnection();
|
||||||
|
} catch (Exception e) {
|
||||||
|
new Popup().information(e.getMessage()).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isChainHeightSyncedWithinToleranceOrShowPopup(CoreMoneroConnectionsService connectionService) {
|
public static boolean isChainHeightSyncedWithinToleranceOrShowPopup(CoreMoneroConnectionsService connectionService) {
|
||||||
if (!connectionService.isChainHeightSyncedWithinTolerance()) {
|
if (!connectionService.isSyncedWithinTolerance()) {
|
||||||
new Popup().information(Res.get("popup.warning.chainNotSynced")).show();
|
new Popup().information(Res.get("popup.warning.chainNotSynced")).show();
|
||||||
return false;
|
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 period_state = 19;
|
||||||
string payout_state = 20;
|
string payout_state = 20;
|
||||||
string dispute_state = 21;
|
string dispute_state = 21;
|
||||||
bool is_deposit_published = 22;
|
bool is_deposits_published = 22;
|
||||||
bool is_deposit_confirmed = 23;
|
bool is_deposits_confirmed = 23;
|
||||||
bool is_deposit_unlocked = 24;
|
bool is_deposits_unlocked = 24;
|
||||||
bool is_payment_sent = 25;
|
bool is_payment_sent = 25;
|
||||||
bool is_payment_received = 26;
|
bool is_payment_received = 26;
|
||||||
bool is_payout_published = 27;
|
bool is_payout_published = 27;
|
||||||
|
|
|
@ -1652,11 +1652,12 @@ message Trade {
|
||||||
repeated ChatMessage chat_message = 22;
|
repeated ChatMessage chat_message = 22;
|
||||||
MediationResultState mediation_result_state = 23;
|
MediationResultState mediation_result_state = 23;
|
||||||
int64 lock_time = 24;
|
int64 lock_time = 24;
|
||||||
NodeAddress refund_agent_node_address = 25;
|
int64 start_time = 25;
|
||||||
RefundResultState refund_result_state = 26;
|
NodeAddress refund_agent_node_address = 26;
|
||||||
string counter_currency_extra_data = 27;
|
RefundResultState refund_result_state = 27;
|
||||||
string asset_tx_proof_result = 28; // name of AssetTxProofResult enum
|
string counter_currency_extra_data = 28;
|
||||||
string uid = 29;
|
string asset_tx_proof_result = 29; // name of AssetTxProofResult enum
|
||||||
|
string uid = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BuyerAsMakerTrade {
|
message BuyerAsMakerTrade {
|
||||||
|
@ -1708,6 +1709,10 @@ message ProcessModel {
|
||||||
TradingPeer arbitrator = 1004;
|
TradingPeer arbitrator = 1004;
|
||||||
NodeAddress temp_trading_peer_node_address = 1005;
|
NodeAddress temp_trading_peer_node_address = 1005;
|
||||||
string multisig_address = 1006;
|
string multisig_address = 1006;
|
||||||
|
|
||||||
|
PaymentSentMessage payment_sent_message = 1012;
|
||||||
|
PaymentReceivedMessage payment_received_message = 1013;
|
||||||
|
DisputeClosedMessage dispute_closed_message = 1014;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TradingPeer {
|
message TradingPeer {
|
||||||
|
@ -1745,7 +1750,6 @@ message TradingPeer {
|
||||||
string deposit_tx_hex = 1009;
|
string deposit_tx_hex = 1009;
|
||||||
string deposit_tx_key = 1010;
|
string deposit_tx_key = 1010;
|
||||||
string updated_multisig_hex = 1011;
|
string updated_multisig_hex = 1011;
|
||||||
PaymentSentMessage payment_sent_message = 1012;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Reference in a new issue