diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java index eb8cb44cae..cfa651dce0 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -16,7 +16,7 @@ import org.junit.jupiter.api.TestInfo; import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static bisq.core.trade.Trade.Phase.DEPOSITS_UNLOCKED; import static bisq.core.trade.Trade.Phase.PAYMENT_SENT; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED; import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG; import static bisq.core.trade.Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; import static bisq.core.trade.Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG; @@ -150,7 +150,7 @@ public class AbstractTradeTest extends AbstractOfferTest { String tradeId) { Predicate isTradeInPaymentReceiptConfirmedStateAndPhase = (t) -> t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name()) && - (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name())); + t.getPhase().equals(PAYMENT_SENT.name()); String userName = toUserName.apply(grpcClient); for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { TradeInfo trade = grpcClient.getTrade(tradeId); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 15688ec6c2..47d90e2c8d 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -30,13 +30,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; -import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.USD; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.OfferDirection.BUY; import static protobuf.OpenOffer.State.AVAILABLE; @@ -113,8 +110,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { trade = bobClient.getTrade(tradeId); // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) - .setPhase(PAYOUT_PUBLISHED) + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) + .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java index 378f62ce15..c19975ce4d 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -50,8 +50,8 @@ import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BTC; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.*; import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.OfferDirection.BUY; @@ -200,8 +200,8 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest { trade = bobClient.getTrade(tradeId); // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) - .setPhase(PAYOUT_PUBLISHED) + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) + .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java index b56b451a13..9fcfdd4bc5 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyXMROfferTest.java @@ -29,11 +29,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; -import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.cli.table.builder.TableType.OFFER_TBL; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.Offer.State.OFFER_FEE_RESERVED; @@ -130,8 +129,8 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest { trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) - .setPhase(PAYOUT_PUBLISHED) + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) + .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index ed38f79402..293bdb94f0 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -32,10 +32,10 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.USD; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.Phase.WITHDRAWN; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; -import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED; +import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED; +import static bisq.core.trade.Trade.Phase.COMPLETED; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; +import static bisq.core.trade.Trade.State.TRADE_COMPLETED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -119,8 +119,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { sleep(3_000); trade = aliceClient.getTrade(tradeId); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) - .setPhase(PAYOUT_PUBLISHED) + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) + .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java index 5d2bcbbced..aa7e313d3d 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellXMROfferTest.java @@ -32,12 +32,9 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.cli.table.builder.TableType.OFFER_TBL; -import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; -import static bisq.core.trade.Trade.Phase.WITHDRAWN; -import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; -import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED; +import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static protobuf.OfferDirection.BUY; @@ -139,8 +136,8 @@ public class TakeSellXMROfferTest extends AbstractTradeTest { trade = bobClient.getTrade(tradeId); // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED. - EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) - .setPhase(PAYOUT_PUBLISHED) + EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG) + .setPhase(PAYMENT_RECEIVED) .setPayoutPublished(true) .setPaymentReceivedMessageSent(true); verifyExpectedProtocolStatus(trade); diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index 57c8fdb969..d6d752a42b 100644 --- a/core/src/main/java/bisq/core/api/CoreDisputesService.java +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -105,9 +105,6 @@ public class CoreDisputesService { String updatedMultisigHex = multisigWallet.exportMultisigHex(); disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler); tradeManager.requestPersistence(); - - // close multisig wallet - xmrWalletService.closeMultisigWallet(trade.getId()); } } @@ -159,7 +156,8 @@ public class CoreDisputesService { if (disputeOptional.isPresent()) dispute = disputeOptional.get(); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); - synchronized (tradeManager.getTrade(tradeId)) { + Trade trade = tradeManager.getTrade(tradeId); + synchronized (trade) { var closeDate = new Date(); var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); var contract = dispute.getContract(); @@ -176,8 +174,8 @@ public class CoreDisputesService { } applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); - // resolve the payout - resolveDisputePayout(dispute, disputeResult, contract); + // apply dispute payout + applyDisputePayout(dispute, disputeResult, contract); // close dispute ticket closeDispute(arbitrationManager, dispute, disputeResult, false); @@ -186,19 +184,17 @@ public class CoreDisputesService { var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() .filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .findFirst(); - if (peersDisputeOptional.isPresent()) { var peerDispute = peersDisputeOptional.get(); var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher()); - resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); + applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); } else { throw new IllegalStateException("could not find peer dispute"); } - arbitrationManager.requestPersistence(); } } catch (Exception e) { @@ -250,7 +246,7 @@ public class CoreDisputesService { } } - public void resolveDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) { + public void applyDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) { // TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master) if (!dispute.isMediationDispute()) { try { @@ -259,30 +255,25 @@ public class CoreDisputesService { //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); - // TODO (woodser): don't send signed tx if opener is not co-signer? - // // determine if opener is co-signer - // boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER); - // boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher(); - // if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx"); + // determine if dispute is in context of publisher + boolean isOpener = dispute.isOpener(); + boolean isWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.SELLER); + boolean isPublisher = disputeResult.isLoserPublisher() ? !isWinner : isWinner; // open multisig wallet MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - // arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex - boolean isOpener = dispute.isOpener(); - System.out.println("Is dispute opener: " + isOpener); + // if dispute is in context of opener, arbitrator has multisig hex to create and validate payout tx if (isOpener) { MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); - if (arbitratorPayoutTx != null) - disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); + + // if opener is publisher, include signed payout tx in dispute result, otherwise publisher must request payout tx by providing updated multisig hex + if (isPublisher) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); } // send arbitrator's updated multisig hex with dispute result disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex()); - - // close multisig wallet - xmrWalletService.closeMultisigWallet(dispute.getTradeId()); } } catch (AddressFormatException e2) { log.error("Error at close dispute", e2); diff --git a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java index b80f978958..bd067cdb0c 100644 --- a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java +++ b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java @@ -5,7 +5,7 @@ import bisq.common.config.Config; import bisq.core.btc.model.EncryptedConnectionList; import bisq.core.btc.setup.DownloadListener; import bisq.core.btc.setup.WalletsSetup; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -239,7 +239,7 @@ public final class CoreMoneroConnectionsService { public long getDefaultRefreshPeriodMs() { if (daemon == null) return REFRESH_PERIOD_LOCAL_MS; else { - boolean isLocal = TradeUtils.isLocalHost(daemon.getRpcConnection().getUri()); + boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri()); if (isLocal) { updateDaemonInfo(); if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped @@ -363,7 +363,7 @@ public final class CoreMoneroConnectionsService { // if offline and last connection is local, start local node if offline currentConnectionUri.ifPresent(uri -> { try { - if (!connectionManager.isConnected() && TradeUtils.isLocalHost(uri) && !nodeService.isOnline()) { + if (!connectionManager.isConnected() && HavenoUtils.isLocalHost(uri) && !nodeService.isOnline()) { nodeService.startMoneroNode(); } } catch (Exception e) { @@ -372,7 +372,7 @@ public final class CoreMoneroConnectionsService { }); // prefer to connect to local node unless prevented by configuration - if (("".equals(config.xmrNode) || TradeUtils.isLocalHost(config.xmrNode)) && + if (("".equals(config.xmrNode) || HavenoUtils.isLocalHost(config.xmrNode)) && (!connectionManager.isConnected() || connectionManager.getAutoSwitch()) && nodeService.isConnected()) { MoneroRpcConnection connection = connectionManager.getConnectionByUri(nodeService.getDaemon().getRpcConnection().getUri()); diff --git a/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java b/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java index 125793de37..dc738342cf 100644 --- a/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java +++ b/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java @@ -16,7 +16,7 @@ */ package bisq.core.api; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.user.Preferences; import bisq.core.xmr.MoneroNodeSettings; import bisq.common.config.BaseCurrencyNetwork; @@ -71,7 +71,7 @@ public class CoreMoneroNodeService { else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081; else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081; else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet"); - this.daemon = new MoneroDaemonRpc("http://" + TradeUtils.LOOPBACK_HOST + ":" + rpcPort); + this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort); } public void addListener(MoneroNodeServiceListener listener) { diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index 63e58be489..8441f31480 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -79,12 +79,13 @@ public class TradeInfo implements Payload { private final String state; private final String phase; private final String periodState; + private final String payoutState; private final boolean isDepositPublished; private final boolean isDepositUnlocked; private final boolean isPaymentSent; private final boolean isPaymentReceived; - private final boolean isPayoutPublished; private final boolean isCompleted; + private final boolean isPayoutPublished; private final String contractAsJson; private final ContractInfo contract; @@ -107,6 +108,7 @@ public class TradeInfo implements Payload { this.state = builder.getState(); this.phase = builder.getPhase(); this.periodState = builder.getPeriodState(); + this.payoutState = builder.getPayoutState(); this.isDepositPublished = builder.isDepositPublished(); this.isDepositUnlocked = builder.isDepositUnlocked(); this.isPaymentSent = builder.isPaymentSent(); @@ -158,6 +160,7 @@ public class TradeInfo implements Payload { .withState(trade.getState().name()) .withPhase(trade.getPhase().name()) .withPeriodState(trade.getPeriodState().name()) + .withPayoutState(trade.getPayoutState().name()) .withIsDepositPublished(trade.isDepositPublished()) .withIsDepositUnlocked(trade.isDepositUnlocked()) .withIsPaymentSent(trade.isPaymentSent()) @@ -195,12 +198,13 @@ public class TradeInfo implements Payload { .setState(state) .setPhase(phase) .setPeriodState(periodState) + .setPayoutState(payoutState) .setIsDepositPublished(isDepositPublished) .setIsDepositUnlocked(isDepositUnlocked) .setIsPaymentSent(isPaymentSent) .setIsPaymentReceived(isPaymentReceived) + .setIsCompleted(isCompleted) .setIsPayoutPublished(isPayoutPublished) - .setIsPayoutPublished(isCompleted) .setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContract(contract.toProtoMessage()) .build(); @@ -222,6 +226,7 @@ public class TradeInfo implements Payload { .withPrice(proto.getPrice()) .withVolume(proto.getTradeVolume()) .withPeriodState(proto.getPeriodState()) + .withPayoutState(proto.getPayoutState()) .withState(proto.getState()) .withPhase(proto.getPhase()) .withArbitratorNodeAddress(proto.getArbitratorNodeAddress()) @@ -230,8 +235,8 @@ public class TradeInfo implements Payload { .withIsDepositUnlocked(proto.getIsDepositUnlocked()) .withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentReceived(proto.getIsPaymentReceived()) - .withIsPayoutPublished(proto.getIsPayoutPublished()) .withIsCompleted(proto.getIsCompleted()) + .withIsPayoutPublished(proto.getIsPayoutPublished()) .withContractAsJson(proto.getContractAsJson()) .withContract((ContractInfo.fromProto(proto.getContract()))) .build(); @@ -256,12 +261,13 @@ public class TradeInfo implements Payload { ", state='" + state + '\'' + "\n" + ", phase='" + phase + '\'' + "\n" + ", periodState='" + periodState + '\'' + "\n" + + ", payoutState='" + payoutState + '\'' + "\n" + ", isDepositPublished=" + isDepositPublished + "\n" + ", isDepositConfirmed=" + isDepositUnlocked + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" + - ", isPayoutPublished=" + isPayoutPublished + "\n" + ", isCompleted=" + isCompleted + "\n" + + ", isPayoutPublished=" + isPayoutPublished + "\n" + ", offer=" + offer + "\n" + ", contractAsJson=" + contractAsJson + "\n" + ", contract=" + contract + "\n" + diff --git a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java index 5b591defad..ceeb3972d5 100644 --- a/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java +++ b/core/src/main/java/bisq/core/api/model/builder/TradeInfoV1Builder.java @@ -51,6 +51,7 @@ public final class TradeInfoV1Builder { private String state; private String phase; private String periodState; + private String payoutState; private boolean isDepositPublished; private boolean isDepositUnlocked; private boolean isPaymentSent; @@ -130,12 +131,7 @@ public final class TradeInfoV1Builder { this.volume = volume; return this; } - - public TradeInfoV1Builder withPeriodState(String periodState) { - this.periodState = periodState; - return this; - } - + public TradeInfoV1Builder withState(String state) { this.state = state; return this; @@ -146,6 +142,16 @@ public final class TradeInfoV1Builder { return this; } + public TradeInfoV1Builder withPeriodState(String periodState) { + this.periodState = periodState; + return this; + } + + public TradeInfoV1Builder withPayoutState(String payoutState) { + this.payoutState = payoutState; + return this; + } + public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) { this.arbitratorNodeAddress = arbitratorNodeAddress; return this; diff --git a/core/src/main/java/bisq/core/app/HavenoExecutable.java b/core/src/main/java/bisq/core/app/HavenoExecutable.java index cab0126d33..1333d1ca69 100644 --- a/core/src/main/java/bisq/core/app/HavenoExecutable.java +++ b/core/src/main/java/bisq/core/app/HavenoExecutable.java @@ -27,6 +27,7 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.setup.CorePersistedDataHost; import bisq.core.setup.CoreSetup; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.trade.TradeManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.network.p2p.P2PService; @@ -279,6 +280,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService log.info("OpenOfferManager shutdown started"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { diff --git a/core/src/main/java/bisq/core/app/HavenoSetup.java b/core/src/main/java/bisq/core/app/HavenoSetup.java index 1fc2f31186..478637f0fc 100644 --- a/core/src/main/java/bisq/core/app/HavenoSetup.java +++ b/core/src/main/java/bisq/core/app/HavenoSetup.java @@ -556,7 +556,6 @@ public class HavenoSetup { return null; } - @Nullable public static boolean getResyncSpvSemaphore() { File resyncSpvSemaphore = new File(Config.appDataDir(), RESYNC_SPV_FILE_NAME); return resyncSpvSemaphore.exists(); diff --git a/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImageListener.java b/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImageListener.java new file mode 100644 index 0000000000..f2accf0e15 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImageListener.java @@ -0,0 +1,13 @@ +package bisq.core.btc.wallet; + +import java.util.Map; + +import monero.daemon.model.MoneroKeyImageSpentStatus; + +public interface MoneroKeyImageListener { + + /** + * Called with changes to the spent status of key images. + */ + public void onSpentStatusChanged(Map spentStatuses); +} diff --git a/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java b/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java new file mode 100644 index 0000000000..a7b7f7de11 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/wallet/MoneroKeyImagePoller.java @@ -0,0 +1,197 @@ +package bisq.core.btc.wallet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import monero.common.MoneroError; +import monero.common.TaskLooper; +import monero.daemon.MoneroDaemon; +import monero.daemon.model.MoneroKeyImageSpentStatus; + +/** + * Poll for changes to the spent status of key images. + */ +public class MoneroKeyImagePoller { + + private MoneroDaemon daemon; + private long refreshPeriodMs; + private List keyImages = new ArrayList(); + private Set listeners = new HashSet(); + private TaskLooper looper; + private Map lastStatuses = new HashMap(); + + /** + * Construct the listener. + * + * @param refreshPeriodMs - refresh period in milliseconds + * @param keyImages - key images to listen to + */ + public MoneroKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs, String... keyImages) { + looper = new TaskLooper(() -> poll()); + setDaemon(daemon); + setRefreshPeriodMs(refreshPeriodMs); + setKeyImages(keyImages); + } + + /** + * Add a listener to receive notifications. + * + * @param listener - the listener to add + */ + public void addListener(MoneroKeyImageListener listener) { + listeners.add(listener); + refreshPolling(); + } + + /** + * Remove a listener to receive notifications. + * + * @param listener - the listener to remove + */ + public void removeListener(MoneroKeyImageListener listener) { + if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); + listeners.remove(listener); + refreshPolling(); + } + + /** + * Set the Monero daemon to fetch key images from. + * + * @param daemon - the daemon to fetch key images from + */ + public void setDaemon(MoneroDaemon daemon) { + this.daemon = daemon; + } + + /** + * Get the Monero daemon to fetch key images from. + * + * @return the daemon to fetch key images from + */ + public MoneroDaemon getDaemon() { + return daemon; + } + + /** + * Set the refresh period in milliseconds. + * + * @param refreshPeriodMs - the refresh period in milliseconds + */ + public void setRefreshPeriodMs(long refreshPeriodMs) { + this.refreshPeriodMs = refreshPeriodMs; + } + + /** + * Get the refresh period in milliseconds + * + * @return the refresh period in milliseconds + */ + public long getRefreshPeriodMs() { + return refreshPeriodMs; + } + + /** + * Get a copy of the key images being listened to. + * + * @return the key images to listen to + */ + public Collection getKeyImages() { + return new ArrayList(keyImages); + } + + /** + * Set the key images to listen to. + * + * @return the key images to listen to + */ + public void setKeyImages(String... keyImages) { + synchronized (keyImages) { + this.keyImages.clear(); + this.keyImages.addAll(Arrays.asList(keyImages)); + refreshPolling(); + } + } + + /** + * Add a key image to listen to. + * + * @param keyImage - the key image to listen to + */ + public void addKeyImage(String keyImage) { + synchronized (keyImages) { + addKeyImages(keyImage); + refreshPolling(); + } + } + + /** + * Add key images to listen to. + * + * @param keyImages - key images to listen to + */ + public void addKeyImages(String... keyImages) { + synchronized (keyImages) { + for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) this.keyImages.add(keyImage); + refreshPolling(); + } + } + + /** + * Remove a key image to listen to. + * + * @param keyImage - the key image to unlisten to + */ + public void removeKeyImage(String keyImage) { + synchronized (keyImages) { + removeKeyImages(keyImage); + refreshPolling(); + } + } + + /** + * Remove key images to listen to. + * + * @param keyImages - key images to unlisten to + */ + public void removeKeyImages(String... keyImages) { + synchronized (keyImages) { + for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) throw new MoneroError("Key image not registered with poller: " + keyImage); + this.keyImages.removeAll(Arrays.asList(keyImages)); + } + } + + public void poll() { + synchronized (keyImages) { + + // fetch spent statuses + List spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); + + // collect changed statuses + Map changedStatuses = new HashMap(); + for (int i = 0; i < keyImages.size(); i++) { + if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) { + lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); + changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); + } + } + + // announce changes + for (MoneroKeyImageListener listener : new ArrayList(listeners)) listener.onSpentStatusChanged(changedStatuses); + } + } + + private void refreshPolling() { + setIsPolling(listeners.size() > 0); + } + + private void setIsPolling(boolean isPolling) { + if (isPolling) looper.start(refreshPeriodMs); + else looper.stop(); + } +} diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index 2272aabeb1..96d21f1671 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -20,8 +20,10 @@ import bisq.core.trade.MakerTrade; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.util.ParsingUtils; + +import com.google.common.collect.TreeMultimap; import com.google.common.util.concurrent.Service.State; import com.google.inject.name.Named; import common.utils.JsonUtils; @@ -29,6 +31,7 @@ import java.io.File; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -47,6 +50,7 @@ import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcError; import monero.common.MoneroUtils; import monero.daemon.MoneroDaemonRpc; +import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroNetworkType; import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroSubmitTxResult; @@ -97,6 +101,7 @@ public class XmrWalletService { private TradeManager tradeManager; private MoneroWalletRpc wallet; private Map multisigWallets; + private Map walletLocks = new HashMap(); private final Map> txCache = new HashMap>(); @Inject @@ -182,29 +187,40 @@ public class XmrWalletService { return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); } - public boolean multisigWalletExists(String tradeId) { - return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId); + private synchronized void initWalletLock(String id) { + if (!walletLocks.containsKey(id)) walletLocks.put(id, new Object()); + } + + public boolean multisigWalletExists(String tradeId) { + initWalletLock(tradeId); + synchronized(walletLocks.get(tradeId)) { + return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId); + } } - // TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse public MoneroWallet createMultisigWallet(String tradeId) { - log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); - if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); - String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, false); // auto-assign port - multisigWallets.put(tradeId, multisigWallet); - return multisigWallet; + initWalletLock(tradeId); + synchronized(walletLocks.get(tradeId)) { + log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); + if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); + String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; + MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port + multisigWallets.put(tradeId, multisigWallet); + return multisigWallet; + } } // TODO (woodser): provide progress notifications during open? public MoneroWallet getMultisigWallet(String tradeId) { - log.info("{}.getMultisigWallet({})", getClass().getSimpleName(), tradeId); - if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); - String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId); - MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); - multisigWallets.put(tradeId, multisigWallet); - return multisigWallet; + initWalletLock(tradeId); + synchronized(walletLocks.get(tradeId)) { + if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); + String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; + if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId); + MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); + multisigWallets.put(tradeId, multisigWallet); + return multisigWallet; + } } public void saveWallet(MoneroWallet wallet) { @@ -213,19 +229,25 @@ public class XmrWalletService { } public void closeMultisigWallet(String tradeId) { - log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); - if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); - MoneroWallet wallet = multisigWallets.remove(tradeId); - closeWallet(wallet, true); + initWalletLock(tradeId); + synchronized(walletLocks.get(tradeId)) { + log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); + if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); + MoneroWallet wallet = multisigWallets.remove(tradeId); + closeWallet(wallet, true); + } } public boolean deleteMultisigWallet(String tradeId) { - log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); - String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - if (!walletExists(walletName)) return false; - if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); // TODO: synchronize - deleteWallet(walletName); - return true; + initWalletLock(tradeId); + synchronized(walletLocks.get(tradeId)) { + log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); + String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; + if (!walletExists(walletName)) return false; + if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); + deleteWallet(walletName); + return true; + } } public MoneroTxWallet createTx(List destinations) { @@ -254,21 +276,20 @@ public class XmrWalletService { // get expected mining fee MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig() .setAccountIndex(0) - .addDestination(TradeUtils.getTradeFeeAddress(), tradeFee) + .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(returnAddress, depositAmount)); BigInteger miningFee = miningFeeTx.getFee(); // create reserve tx MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig() .setAccountIndex(0) - .addDestination(TradeUtils.getTradeFeeAddress(), tradeFee) + .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit? // freeze inputs if (freezeInputs) { - for (MoneroOutput input : reserveTx.getInputs()) { - wallet.freezeOutput(input.getKeyImage().getHex()); - } + for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex()); + wallet.save(); } return reserveTx; @@ -291,13 +312,12 @@ public class XmrWalletService { // create deposit tx MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig() .setAccountIndex(0) - .addDestination(TradeUtils.getTradeFeeAddress(), tradeFee) + .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) .addDestination(multisigAddress, depositAmount)); // freeze deposit inputs - for (MoneroOutput input : depositTx.getInputs()) { - wallet.freezeOutput(input.getKeyImage().getHex()); - } + for (MoneroOutput input : depositTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex()); + wallet.save(); return depositTx; } @@ -342,7 +362,7 @@ public class XmrWalletService { if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); // verify trade fee - String feeAddress = TradeUtils.getTradeFeeAddress(); + String feeAddress = HavenoUtils.getTradeFeeAddress(); MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); @@ -435,10 +455,8 @@ public class XmrWalletService { // initialize main wallet if connected or previously created maybeInitMainWallet(); - // update wallet connections on change - connectionsService.addListener(newConnection -> { - setWalletDaemonConnections(newConnection); - }); + // set and listen to daemon connection + connectionsService.addListener(newConnection -> setDaemonConnection(newConnection)); } private boolean walletExists(String walletName) { @@ -580,17 +598,13 @@ public class XmrWalletService { return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); } - private void setWalletDaemonConnections(MoneroRpcConnection connection) { + private void setDaemonConnection(MoneroRpcConnection connection) { log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri())); if (wallet == null) maybeInitMainWallet(); if (wallet != null) { wallet.setDaemonConnection(connection); wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); } - for (MoneroWallet multisigWallet : multisigWallets.values()) { - multisigWallet.setDaemonConnection(connection); - multisigWallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); // TODO: optimize when multisig wallets are open and syncing - } } private void notifyBalanceListeners() { @@ -663,7 +677,7 @@ public class XmrWalletService { if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - deleteBackupWallets(walletName); + deleteBackupWallets(walletName); // TODO: retain backup for some time? } private void closeAllWallets() { @@ -675,30 +689,16 @@ public class XmrWalletService { openWallets.add(multisigWallets.get(multisigWalletKey)); } - // done if no open wallets - if (openWallets.isEmpty()) return; - - // close all wallets in parallel - ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, openWallets.size())); - for (MoneroWallet openWallet : openWallets) { - pool.submit(new Runnable() { - @Override - public void run() { - try { - closeWallet(openWallet, true); - } catch (Exception e) { - log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); - } - } - }); - } - pool.shutdown(); - try { - if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); - } catch (InterruptedException e) { - pool.shutdownNow(); - throw new RuntimeException(e); - } + // close wallets in parallel + Set tasks = new HashSet(); + for (MoneroWallet wallet : openWallets) tasks.add(() -> { + try { + closeWallet(wallet, true); + } catch (Exception e) { + log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); + } + }); + HavenoUtils.awaitTasks(tasks); // clear wallets wallet = null; diff --git a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java index 8731e309fc..065e6e7253 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java @@ -82,13 +82,11 @@ public class TradeEvents { msg = Res.get("account.notifications.trade.message.msg.started", shortId); break; case PAYMENT_RECEIVED: - break; - case PAYOUT_PUBLISHED: // We only notify the buyer if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.completed", shortId); break; - case WITHDRAWN: + case COMPLETED: break; } if (msg != null) { diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index 95f2c04470..32b4a4b975 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -23,12 +23,10 @@ import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.monetary.Price; -import bisq.core.offer.availability.DisputeAgentSelection; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountUtil; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; -import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; @@ -48,9 +46,6 @@ import org.bitcoinj.core.Coin; import javax.inject.Inject; import javax.inject.Singleton; -import com.google.common.collect.Lists; - -import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; diff --git a/core/src/main/java/bisq/core/offer/OfferFilterService.java b/core/src/main/java/bisq/core/offer/OfferFilterService.java index 8bf69ef992..a4fa2a689a 100644 --- a/core/src/main/java/bisq/core/offer/OfferFilterService.java +++ b/core/src/main/java/bisq/core/offer/OfferFilterService.java @@ -22,7 +22,7 @@ import bisq.core.filter.FilterManager; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountUtil; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.user.Preferences; import bisq.core.user.User; @@ -224,6 +224,6 @@ public class OfferFilterService { public boolean hasValidSignature(Offer offer) { Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); if (arbitrator == null) return false; // invalid arbitrator - return TradeUtils.isArbitratorSignatureValid(offer, arbitrator); + return HavenoUtils.isArbitratorSignatureValid(offer, arbitrator); } } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index c79b3ed480..afdf196a2c 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -36,7 +36,7 @@ import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.TradableList; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.handlers.TransactionResultHandler; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; @@ -564,6 +564,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Offer offer = openOffer.getOffer(); if (offer.getOfferPayload().getReserveTxKeyImages() != null) { for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage); + xmrWalletService.getWallet().save(); } offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); @@ -634,7 +635,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe latch.countDown(); errorMessages.add(errorMessage); }); - TradeUtils.awaitLatch(latch); + HavenoUtils.awaitLatch(latch); } requestPersistence(); if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString()); diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java index 7a601de998..a953ad9145 100644 --- a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java +++ b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java @@ -21,7 +21,7 @@ import bisq.core.offer.AvailabilityResult; import bisq.core.offer.Offer; import bisq.core.offer.availability.OfferAvailabilityModel; import bisq.core.offer.messages.OfferAvailabilityResponse; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.common.taskrunner.Task; import bisq.common.taskrunner.TaskRunner; @@ -54,7 +54,7 @@ public class ProcessOfferAvailabilityResponse extends Task { handleError(Res.get("createOffer.timeoutAtPublishing")); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerProcessSignOfferResponse.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerProcessSignOfferResponse.java index 8231a19b15..34e0456d22 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerProcessSignOfferResponse.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerProcessSignOfferResponse.java @@ -20,7 +20,7 @@ package bisq.core.offer.placeoffer.tasks; import bisq.core.offer.Offer; import bisq.core.offer.placeoffer.PlaceOfferModel; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import static com.google.common.base.Preconditions.checkNotNull; @@ -42,7 +42,7 @@ public class MakerProcessSignOfferResponse extends Task { Arbitrator arbitrator = checkNotNull(model.getUser().getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedArbitratorByAddress(arbitratorSigner) must not be null"); // validate arbitrator signature - if (!TradeUtils.isArbitratorSignatureValid(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), arbitrator)) { + if (!HavenoUtils.isArbitratorSignatureValid(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), arbitrator)) { throw new RuntimeException("Offer payload has invalid arbitrator signature"); } diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index d0bd3b7c1a..bb8acd88e3 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -39,22 +39,18 @@ import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.messages.PaymentSentMessage; -import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; -import bisq.core.trade.messages.PaymentAccountKeyRequest; -import bisq.core.trade.messages.PaymentAccountKeyResponse; +import bisq.core.trade.messages.DepositsConfirmedMessage; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.RefreshTradeStateRequest; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TraderSignedWitnessMessage; -import bisq.core.trade.messages.UpdateMultisigRequest; -import bisq.core.trade.messages.UpdateMultisigResponse; import bisq.network.p2p.AckMessage; import bisq.network.p2p.BundleOfEnvelopes; @@ -158,21 +154,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo return DepositRequest.fromProto(proto.getDepositRequest(), this, messageVersion); case DEPOSIT_RESPONSE: return DepositResponse.fromProto(proto.getDepositResponse(), this, messageVersion); - case PAYMENT_ACCOUNT_KEY_REQUEST: - return PaymentAccountKeyRequest.fromProto(proto.getPaymentAccountKeyRequest(), this, messageVersion); - case PAYMENT_ACCOUNT_KEY_RESPONSE: - return PaymentAccountKeyResponse.fromProto(proto.getPaymentAccountKeyResponse(), this, messageVersion); - case UPDATE_MULTISIG_REQUEST: - return UpdateMultisigRequest.fromProto(proto.getUpdateMultisigRequest(), this, messageVersion); - case UPDATE_MULTISIG_RESPONSE: - return UpdateMultisigResponse.fromProto(proto.getUpdateMultisigResponse(), this, messageVersion); + case DEPOSITS_CONFIRMED_MESSAGE: + return DepositsConfirmedMessage.fromProto(proto.getDepositsConfirmedMessage(), this, messageVersion); case PAYMENT_SENT_MESSAGE: return PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion); case PAYMENT_RECEIVED_MESSAGE: return PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), messageVersion); - case PAYOUT_TX_PUBLISHED_MESSAGE: - return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion); case TRADER_SIGNED_WITNESS_MESSAGE: return TraderSignedWitnessMessage.fromProto(proto.getTraderSignedWitnessMessage(), messageVersion); diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index ff1ff7ada2..d7542469ab 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -333,13 +333,10 @@ public abstract class DisputeManager> extends Sup if (isAgent(dispute)) { // update arbitrator's multisig wallet - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - multisigWallet.importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex()); + trade.syncWallet(); + trade.getWallet().importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex()); + trade.saveWallet(); log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId()); - - // close multisig wallet - xmrWalletService.closeMultisigWallet(dispute.getTradeId()); - synchronized (disputeList) { if (!disputeList.contains(dispute)) { Optional storedDisputeOptional = findDispute(dispute); @@ -748,6 +745,15 @@ public abstract class DisputeManager> extends Sup disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), chatMessage.getUid()); + // TODO: hack to sync wallet after dispute message received in order to detect payout published + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); + for (int i = 0; i < 3; i++) { + UserThread.runAfter(() -> { + if (!trade.isPayoutUnlocked()) trade.syncWallet(); + }, defaultRefreshPeriod / 1000 * (i + 1)); + } + // We use the chatMessage wrapped inside the disputeResultMessage for // the state, as that is displayed to the user and we only persist that msg chatMessage.setArrived(true); diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index 3c00c503b4..f60c67aaae 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -301,7 +301,7 @@ public final class ArbitrationManager extends DisputeManager { + if (!trade.isPayoutUnlocked()) trade.syncWallet(); + }, defaultRefreshPeriod / 1000 * (i + 1)); + } } @Override @@ -546,6 +555,7 @@ public final class ArbitrationManager extends DisputeManager openOfferManager.closeOpenOffer(openOffer.getOffer())); } } - // dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) { ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest( diff --git a/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java index 6fe34b8c66..68ae1a7df6 100644 --- a/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java +++ b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java @@ -64,7 +64,7 @@ public class TradeChatSession extends SupportSession { @Override public boolean chatIsOpen() { - return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED; + return trade != null && trade.getState() != Trade.State.TRADE_COMPLETED; } @Override diff --git a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java index 5b0796d43b..be2c719010 100644 --- a/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java +++ b/core/src/main/java/bisq/core/trade/ClosedTradableFormatter.java @@ -158,7 +158,7 @@ public class ClosedTradableFormatter { if (isBisqV1Trade(tradable)) { Trade trade = castToTrade(tradable); - if (trade.isWithdrawn() || trade.isPayoutPublished()) { + if (trade.isCompleted() || trade.isPayoutPublished()) { return Res.get("portfolio.closed.completed"); } else if (trade.getDisputeState() == DISPUTE_CLOSED) { return Res.get("portfolio.closed.ticketClosed"); diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/HavenoUtils.java similarity index 65% rename from core/src/main/java/bisq/core/trade/TradeUtils.java rename to core/src/main/java/bisq/core/trade/HavenoUtils.java index f9712ba558..266e064e69 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtils.java +++ b/core/src/main/java/bisq/core/trade/HavenoUtils.java @@ -18,24 +18,24 @@ package bisq.core.trade; import bisq.common.config.Config; -import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.Sig; -import bisq.common.util.Tuple2; -import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.trade.messages.InitTradeRequest; import bisq.core.util.JsonUtil; import java.net.URI; -import java.util.Objects; +import java.util.Collection; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** - * Collection of utilities for trading. + * Collection of utilities. */ -public class TradeUtils { +public class HavenoUtils { public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node public static final String LOCALHOST = "localhost"; @@ -148,61 +148,6 @@ public class TradeUtils { return false; } } - - // TODO (woodser): remove the following utitilites? - - // Returns if both are AVAILABLE, otherwise null - static Tuple2 getAvailableAddresses(Trade trade, XmrWalletService xmrWalletService, - KeyRing keyRing) { - var addresses = getTradeAddresses(trade, xmrWalletService, keyRing); - if (addresses == null) - return null; - - if (xmrWalletService.getAvailableAddressEntries().stream() - .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first))) - return null; - if (xmrWalletService.getAvailableAddressEntries().stream() - .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second))) - return null; - - return new Tuple2<>(addresses.first, addresses.second); - } - - // Returns addresses as strings if they're known by the wallet - public static Tuple2 getTradeAddresses(Trade trade, XmrWalletService xmrWalletService, - KeyRing keyRing) { - var contract = trade.getContract(); - if (contract == null) - return null; - - // TODO (woodser): xmr multisig does not use pub key - throw new RuntimeException("need to replace btc multisig pub key with xmr"); - - // Get multisig address -// var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing()); -// var multiSigPubKey = isMyRoleBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey(); -// if (multiSigPubKey == null) -// return null; -// var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey); -// var multiSigAddress = xmrWalletService.getAddressEntryListAsImmutableList().stream() -// .filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString)) -// .findAny() -// .orElse(null); -// if (multiSigAddress == null) -// return null; -// -// // Get payout address -// var payoutAddress = isMyRoleBuyer ? -// contract.getBuyerPayoutAddressString() : contract.getSellerPayoutAddressString(); -// var payoutAddressEntry = xmrWalletService.getAddressEntryListAsImmutableList().stream() -// .filter(e -> Objects.equals(e.getAddressString(), payoutAddress)) -// .findAny() -// .orElse(null); -// if (payoutAddressEntry == null) -// return null; -// -// return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress); - } public static void awaitLatch(CountDownLatch latch) { try { @@ -211,4 +156,17 @@ public class TradeUtils { throw new RuntimeException(e); } } + + public static void awaitTasks(Collection tasks) { + if (tasks.isEmpty()) return; + ExecutorService pool = Executors.newFixedThreadPool(tasks.size()); + for (Runnable task : tasks) pool.submit(task); + pool.shutdown(); + try { + if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow(); + } catch (InterruptedException e) { + pool.shutdownNow(); + throw new RuntimeException(e); + } + } } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index d51f852d14..07ada1a917 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -50,9 +50,9 @@ import bisq.common.util.Utilities; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.Message; -import common.utils.GenUtils; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Transaction; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; @@ -88,13 +88,14 @@ import static com.google.common.base.Preconditions.checkNotNull; import monero.common.MoneroError; +import monero.common.MoneroRpcConnection; +import monero.common.TaskLooper; import monero.daemon.MoneroDaemon; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; -import monero.wallet.model.MoneroCheckTx; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroMultisigSignResult; -import monero.wallet.model.MoneroTransferQuery; +import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxSet; @@ -125,9 +126,8 @@ public abstract class Trade implements Tradable, Model { // deposit requested SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), - SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), - STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), // not a mailbox msg, not used... remove SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), + SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), // deposit published ARBITRATOR_PUBLISHED_DEPOSIT_TXS(Phase.DEPOSITS_PUBLISHED), @@ -142,30 +142,20 @@ public abstract class Trade implements Tradable, Model { // payment sent BUYER_CONFIRMED_IN_UI_PAYMENT_SENT(Phase.PAYMENT_SENT), BUYER_SENT_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), - BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), - BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), BUYER_SEND_FAILED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), + BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), + BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), SELLER_RECEIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), // payment received SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT(Phase.PAYMENT_RECEIVED), SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), - SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), - SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), - - // payout published - SELLER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED), // TODO (woodser): this enum is over used, like during arbitration - SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), - SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), - SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), - SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), - BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED), - BUYER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED), - PAYOUT_TX_SEEN_IN_NETWORK(Phase.PAYOUT_PUBLISHED), + SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), + SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), // trade completed - WITHDRAW_COMPLETED(Phase.WITHDRAWN); + TRADE_COMPLETED(Phase.COMPLETED); @NotNull public Phase getPhase() { @@ -199,14 +189,13 @@ public abstract class Trade implements Tradable, Model { public enum Phase { INIT, - DEPOSIT_REQUESTED, // TODO (woodser): remove unused phases + DEPOSIT_REQUESTED, DEPOSITS_PUBLISHED, DEPOSITS_CONFIRMED, DEPOSITS_UNLOCKED, PAYMENT_SENT, PAYMENT_RECEIVED, - PAYOUT_PUBLISHED, - WITHDRAWN; + COMPLETED; public static Trade.Phase fromProto(protobuf.Trade.Phase phase) { return ProtoUtil.enumFromProto(Trade.Phase.class, phase.name()); @@ -224,6 +213,25 @@ public abstract class Trade implements Tradable, Model { } } + public enum PayoutState { + UNPUBLISHED, + PUBLISHED, + CONFIRMED, + UNLOCKED; + + public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) { + return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name()); + } + + public static protobuf.Trade.PayoutState toProtoMessage(Trade.PayoutState state) { + return protobuf.Trade.PayoutState.valueOf(state.name()); + } + + public boolean isValidTransitionTo(PayoutState newState) { + return newState.ordinal() > this.ordinal(); + } + } + public enum DisputeState { NO_DISPUTE, // arbitration @@ -307,7 +315,6 @@ public abstract class Trade implements Tradable, Model { private long takeOfferDate; // Mutable - @Nullable @Getter @Setter private long amountAsLong; @@ -317,6 +324,8 @@ public abstract class Trade implements Tradable, Model { @Getter private State state = State.PREPARATION; @Getter + private PayoutState payoutState = PayoutState.UNPUBLISHED; + @Getter private DisputeState disputeState = DisputeState.NO_DISPUTE; @Getter private TradePeriodState periodState = TradePeriodState.FIRST_HALF; @@ -351,11 +360,17 @@ public abstract class Trade implements Tradable, Model { transient final private XmrWalletService xmrWalletService; transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); - transient final private ObjectProperty statePhaseProperty = new SimpleObjectProperty<>(state.phase); + transient final private ObjectProperty phaseProperty = new SimpleObjectProperty<>(state.phase); + transient final private ObjectProperty payoutStateProperty = new SimpleObjectProperty<>(payoutState); transient final private ObjectProperty disputeStateProperty = new SimpleObjectProperty<>(disputeState); transient final private ObjectProperty tradePeriodStateProperty = new SimpleObjectProperty<>(periodState); transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); - + transient private Subscription tradePhaseSubscription = null; + transient private Subscription payoutStateSubscription = null; + transient private TaskLooper tradeTxsLooper; + transient private Long lastWalletRefreshPeriod; + private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour + // Mutable @Getter transient private boolean isInitialized; @@ -530,6 +545,7 @@ public abstract class Trade implements Tradable, Model { .setAmountAsLong(amountAsLong) .setPrice(price) .setState(Trade.State.toProtoMessage(state)) + .setPayoutState(Trade.PayoutState.toProtoMessage(payoutState)) .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) .setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState)) .addAllChatMessage(chatMessages.stream() @@ -556,6 +572,7 @@ public abstract class Trade implements Tradable, Model { public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { trade.setTakeOfferDate(proto.getTakeOfferDate()); trade.setState(State.fromProto(proto.getState())); + trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState())); trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState())); trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); @@ -590,7 +607,56 @@ public abstract class Trade implements Tradable, Model { getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); }); - isInitialized = true; + isInitialized = true; // TODO: move to end? + + // listen to daemon connection + xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection)); + + // done if payout unlocked + if (isPayoutUnlocked()) return; + + // handle trade state events + if (isDepositPublished()) listenToTradeTxs(); + tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { + updateTxListenerRefreshPeriod(); + if (isDepositPublished()) listenToTradeTxs(); + if (isCompleted()) { + UserThread.execute(() -> { + if (tradePhaseSubscription != null) { + tradePhaseSubscription.unsubscribe(); + tradePhaseSubscription = null; + } + }); + } + }); + + // handle payout state events + payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> { + updateTxListenerRefreshPeriod(); + + // cleanup when payout published + if (isPayoutPublished()) { + log.info("Payout published for {} {}", getClass().getSimpleName(), getId()); + if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // complete arbitrator trade when payout published + processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId()); + } + + // cleanup when payout unlocks + if (isPayoutUnlocked()) { + log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time? + deleteWallet(); + if (tradeTxsLooper != null) { + tradeTxsLooper.stop(); + tradeTxsLooper = null; + } + UserThread.execute(() -> { + if (payoutStateSubscription != null) { + payoutStateSubscription.unsubscribe(); + payoutStateSubscription = null; + } + }); + } + }); } @@ -603,12 +669,12 @@ public abstract class Trade implements Tradable, Model { } public NodeAddress getTradingPeerNodeAddress() { - return getTradingPeer() == null ? null : getTradingPeer().getNodeAddress(); + return getTradingPeer() == null ? null : getTradingPeer().getNodeAddress(); } public NodeAddress getArbitratorNodeAddress() { return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); - } + } /** * Create a contract based on the current state. @@ -761,9 +827,8 @@ public abstract class Trade implements Tradable, Model { // submit payout tx if (publish) { multisigWallet.submitMultisigTxHex(payoutTxHex); - setState(isArbitrator() ? Trade.State.WITHDRAW_COMPLETED : isBuyer() ? Trade.State.BUYER_PUBLISHED_PAYOUT_TX : Trade.State.SELLER_PUBLISHED_PAYOUT_TX); + setPayoutState(Trade.PayoutState.PUBLISHED); } - walletService.closeMultisigWallet(getId()); } /** @@ -771,7 +836,7 @@ public abstract class Trade implements Tradable, Model { * * @param paymentAccountKey is the key to decrypt the payment account payload */ - public void decryptPeersPaymentAccountPayload(byte[] paymentAccountKey) { + public void decryptPeerPaymentAccountPayload(byte[] paymentAccountKey) { try { // decrypt payment account payload @@ -792,139 +857,6 @@ public abstract class Trade implements Tradable, Model { } } - /** - * Listen for deposit transactions to unlock and then apply the transactions. - * - * TODO: adopt for general purpose scheduling - * TODO: check and notify if deposits are dropped due to re-org - */ - public void listenForDepositTxs() { - log.info("Listening for deposit txs to unlock for trade {}", getId()); - - // ignore if already listening - if (depositTxListener != null) { - log.warn("Trade {} already listening for deposit txs", getId()); - return; - } - - // get daemon and primary wallet - MoneroWallet havenoWallet = processModel.getXmrWalletService().getWallet(); - - // fetch deposit txs from daemon - List txs = xmrWalletService.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())); - - // handle deposit txs seen - if (txs.size() == 2) { - setStateDepositsPublished(); - boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); - getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); - getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); - - // check if deposit txs unlocked - if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) { - setStateDepositsConfirmed(); - long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK; - if (havenoWallet.getHeight() >= unlockHeight) { - setStateDepositsUnlocked(); - return; - } - } - } - - // create block listener - depositTxListener = new MoneroWalletListener() { - Long unlockHeight = null; - - @Override - public void onNewBlock(long height) { - - // skip if no longer listening - if (depositTxListener == null) return; - - // use latest height - height = havenoWallet.getHeight(); - - // skip if before unlock height - if (unlockHeight != null && height < unlockHeight) return; - - // fetch txs from daemon - List txs = xmrWalletService.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())); - - // skip if deposit txs not seen - if (txs.size() != 2) return; - setStateDepositsPublished(); - - // update deposit txs - boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); - getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); - getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); - - // check if deposit txs confirmed and compute unlock height - if (txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed() && unlockHeight == null) { - log.info("Multisig deposits confirmed for trade {}", getId()); - setStateDepositsConfirmed(); - unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK; - } - - // check if deposit txs unlocked - if (unlockHeight != null && height >= unlockHeight) { - log.info("Multisig deposits unlocked for trade {}", getId()); - xmrWalletService.removeWalletListener(depositTxListener); // remove listener when notified - depositTxListener = null; // prevent re-applying trade state in subsequent requests - setStateDepositsUnlocked(); - } - } - }; - - // register wallet listener - xmrWalletService.addWalletListener(depositTxListener); - } - - public void listenForPayoutTx() { - log.info("Listening for payout tx for trade {}", getId()); - - // check if payout tx already seen - if (getState().ordinal() >= Trade.State.PAYOUT_TX_SEEN_IN_NETWORK.ordinal()) { - log.warn("We had a payout tx already set. tradeId={}, state={}", getId(), getState()); - return; - } - - // get payout address entry - Optional optionalPayoutEntry = xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT); - if (!optionalPayoutEntry.isPresent()) throw new RuntimeException("Trade does not have address entry for payout"); - XmrAddressEntry payoutEntry = optionalPayoutEntry.get(); - - // watch for payout tx on loop - new Thread(() -> { // TODO: use thread manager - boolean found = false; - while (!found) { - if (getPayoutTxKey() != null) { - - // get txs to payout address - List txs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery() - .setTransferQuery(new MoneroTransferQuery() - .setAccountIndex(0) - .setSubaddressIndex(payoutEntry.getSubaddressIndex()) - .setIsIncoming(true))); - - // check for payout tx - for (MoneroTxWallet tx : txs) { - MoneroCheckTx txCheck = xmrWalletService.getWallet().checkTxKey(tx.getHash(), getPayoutTxKey(), payoutEntry.getAddressString()); - if (txCheck.isGood() && txCheck.receivedAmount.compareTo(new BigInteger("0")) > 0) { - found = true; - setPayoutTx(tx); - setStateIfValidTransitionTo(Trade.State.PAYOUT_TX_SEEN_IN_NETWORK); - return; - } - } - } - - // wait to loop - GenUtils.waitFor(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs()); - } - }).start(); - } - @Nullable public MoneroTx getTakerDepositTx() { String depositTxHash = getProcessModel().getTaker().getDepositTxHash(); @@ -984,6 +916,30 @@ public abstract class Trade implements Tradable, Model { } } + public MoneroWallet getWallet() { + return xmrWalletService.multisigWalletExists(getId()) ? xmrWalletService.getMultisigWallet(getId()) : null; + } + + public void syncWallet() { + log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId()); + getWallet().sync(); + log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); + pollWallet(); + } + + public void saveWallet() { + xmrWalletService.saveWallet(getWallet()); + } + + public void deleteWallet() { + if (xmrWalletService.multisigWalletExists(getId())) xmrWalletService.deleteMultisigWallet(getId()); + else log.warn("Multisig wallet to delete for trade {} does not exist", getId()); + } + + public void shutDown() { + isInitialized = false; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Model implementation /////////////////////////////////////////////////////////////////////////////////////////// @@ -1055,7 +1011,37 @@ public abstract class Trade implements Tradable, Model { this.state = state; UserThread.execute(() -> { stateProperty.set(state); - statePhaseProperty.set(state.getPhase()); + phaseProperty.set(state.getPhase()); + }); + } + + public void setStateIfProgress(State state) { + if (state.ordinal() > getState().ordinal()) setState(state); + } + + public void setPayoutStateIfValidTransitionTo(PayoutState newPayoutState) { + if (payoutState.isValidTransitionTo(newPayoutState)) { + setPayoutState(newPayoutState); + } else { + log.warn("Payout state change is not getting applied because it would cause an invalid transition. " + + "Trade payout state={}, intended payout state={}", payoutState, newPayoutState); + } + } + + public void setPayoutState(PayoutState payoutState) { + if (isInitialized) { + // We don't want to log at startup the setState calls from all persisted trades + log.info("Set new payout state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), payoutState); + } + if (payoutState.ordinal() < this.payoutState.ordinal()) { + String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" + + "Old payout state is: " + this.state + ". New payout state is: " + payoutState; + log.warn(message); + } + + this.payoutState = payoutState; + UserThread.execute(() -> { + payoutStateProperty.set(payoutState); }); } @@ -1264,7 +1250,7 @@ public abstract class Trade implements Tradable, Model { return getState().getPhase().ordinal() == Phase.INIT.ordinal(); } - public boolean isTakerFeePublished() { + public boolean isDepositRequested() { return getState().getPhase().ordinal() >= Phase.DEPOSIT_REQUESTED.ordinal(); } @@ -1319,16 +1305,20 @@ public abstract class Trade implements Tradable, Model { return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); } - public boolean isPayoutPublished() { - return getState().getPhase().ordinal() >= Phase.PAYOUT_PUBLISHED.ordinal() || isWithdrawn(); - } - public boolean isCompleted() { - return isPayoutPublished(); + return getState().getPhase().ordinal() >= Phase.COMPLETED.ordinal(); } - public boolean isWithdrawn() { - return getState().getPhase().ordinal() == Phase.WITHDRAWN.ordinal(); + public boolean isPayoutPublished() { + return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal(); + } + + public boolean isPayoutConfirmed() { + return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal(); + } + + public boolean isPayoutUnlocked() { + return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal(); } public ReadOnlyObjectProperty stateProperty() { @@ -1336,7 +1326,11 @@ public abstract class Trade implements Tradable, Model { } public ReadOnlyObjectProperty statePhaseProperty() { - return statePhaseProperty; + return phaseProperty; + } + + public ReadOnlyObjectProperty payoutStateProperty() { + return payoutStateProperty; } public ReadOnlyObjectProperty disputeStateProperty() { @@ -1439,6 +1433,98 @@ public abstract class Trade implements Tradable, Model { return tradeVolumeProperty; } + private void listenToTradeTxs() { + if (tradeTxsLooper != null) return; + log.info("Listening for payout tx for {} {}", getClass().getSimpleName(), getId()); + + // poll wallet for tx state + pollWallet(); + tradeTxsLooper = new TaskLooper(() -> { + try { + pollWallet(); + } catch (Exception e) { + if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage()); + } + }); + tradeTxsLooper.start(getWalletRefreshPeriod()); + } + + private void pollWallet() { + + // skip if payout unlocked + if (isPayoutUnlocked()) return; + + // rescan spent if deposits unlocked + if (isDepositUnlocked()) getWallet().rescanSpent(); + + // get txs with outputs + List txs = getWallet().getTxs(new MoneroTxQuery() + .setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())) + .setIncludeOutputs(true)); + + // check deposit txs + if (!isDepositUnlocked()) { + if (txs.size() == 2) { + setStateDepositsPublished(); + boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); + getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); + getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); + + // check if deposit txs confirmed + if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed(); + if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked(); + } + } + + // check payout tx + else { + + // check if deposit txs spent (appears on payout published) + for (MoneroTxWallet tx : txs) { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (Boolean.TRUE.equals(output.isSpent())) { + setPayoutStatePublished(); + } + } + } + + // check for outgoing txs (appears on payout confirmed) + List outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true)); + if (!outgoingTxs.isEmpty()) { + MoneroTxWallet payoutTx = outgoingTxs.get(0); + setPayoutTx(payoutTx); + setPayoutStatePublished(); + if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); + if (!payoutTx.isLocked()) setPayoutStateUnlocked(); + } + } + } + + private void setDaemonConnection(MoneroRpcConnection connection) { + log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri()); + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId()); + multisigWallet.setDaemonConnection(connection); + multisigWallet.startSyncing(getWalletRefreshPeriod()); + updateTxListenerRefreshPeriod(); + } + + private void updateTxListenerRefreshPeriod() { + long walletRefreshPeriod = getWalletRefreshPeriod(); + if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return; + log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod); + lastWalletRefreshPeriod = walletRefreshPeriod; + if (tradeTxsLooper != null) { + tradeTxsLooper.stop(); + tradeTxsLooper = null; + listenToTradeTxs(); + } + } + + private long getWalletRefreshPeriod() { + if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // arbitrator slows trade wallet after deposits confirm since messages are expected so this is only backup + return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate + } + private void setStateDepositsPublished() { if (!isDepositPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); } @@ -1451,6 +1537,18 @@ public abstract class Trade implements Tradable, Model { if (!isDepositUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); } + private void setPayoutStatePublished() { + if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED); + } + + private void setPayoutStateConfirmed() { + if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED); + } + + private void setPayoutStateUnlocked() { + if (!isPayoutUnlocked()) setPayoutState(PayoutState.UNLOCKED); + } + @Override public String toString() { return "Trade{" + @@ -1463,6 +1561,7 @@ public abstract class Trade implements Tradable, Model { ",\n tradeAmountAsLong=" + amountAsLong + ",\n tradePrice=" + price + ",\n state=" + state + + ",\n payoutState=" + payoutState + ",\n disputeState=" + disputeState + ",\n tradePeriodState=" + periodState + ",\n contract=" + contract + @@ -1477,7 +1576,7 @@ public abstract class Trade implements Tradable, Model { ",\n takerFee=" + takerFee + ",\n xmrWalletService=" + xmrWalletService + ",\n stateProperty=" + stateProperty + - ",\n statePhaseProperty=" + statePhaseProperty + + ",\n statePhaseProperty=" + phaseProperty + ",\n disputeStateProperty=" + disputeStateProperty + ",\n tradePeriodStateProperty=" + tradePeriodStateProperty + ",\n errorMessageProperty=" + errorMessageProperty + diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 08eb7d48bb..329df241a4 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -41,10 +41,8 @@ import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.messages.PaymentAccountKeyRequest; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; -import bisq.core.trade.messages.UpdateMultisigRequest; import bisq.core.trade.protocol.ArbitratorProtocol; import bisq.core.trade.protocol.MakerProtocol; import bisq.core.trade.protocol.ProcessModel; @@ -66,7 +64,6 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.network.TorNetworkNode; import com.google.common.collect.ImmutableList; import bisq.common.ClockWatcher; -import bisq.common.config.Config; import bisq.common.crypto.KeyRing; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.FaultHandler; @@ -245,10 +242,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi handleDepositRequest((DepositRequest) networkEnvelope, peer); } else if (networkEnvelope instanceof DepositResponse) { handleDepositResponse((DepositResponse) networkEnvelope, peer); - } else if (networkEnvelope instanceof PaymentAccountKeyRequest) { - handlePaymentAccountKeyRequest((PaymentAccountKeyRequest) networkEnvelope, peer); - } else if (networkEnvelope instanceof UpdateMultisigRequest) { - handleUpdateMultisigRequest((UpdateMultisigRequest) networkEnvelope, peer); } } @@ -284,6 +277,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi thawUnreservedOutputs(); } + public void shutDown() { + + // collect trades to shutdown + Set trades = new HashSet(); + trades.addAll(tradableList.getList()); + trades.addAll(closedTradableManager.getClosedTrades()); + trades.addAll(failedTradesManager.getObservableList()); + + // shut down trades in parallel + Set tasks = new HashSet(); + for (Trade trade : trades) tasks.add(() -> { + try { + trade.shutDown(); + } catch (Exception e) { + log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?"); + } + }); + HavenoUtils.awaitTasks(tasks); + } + private void thawUnreservedOutputs() { if (xmrWalletService.getWallet() == null) return; @@ -301,13 +314,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi Set frozenKeyImages = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery() .setIsFrozen(true) .setIsSpent(false)) - .stream().map(output -> output.getKeyImage().getHex()) + .stream() + .map(output -> output.getKeyImage().getHex()) .collect(Collectors.toSet()); frozenKeyImages.removeAll(reservedKeyImages); for (String unreservedFrozenKeyImage : frozenKeyImages) { log.info("Thawing output which is not reserved for offer or trade: " + unreservedFrozenKeyImage); xmrWalletService.getWallet().thawOutput(unreservedFrozenKeyImage); } + xmrWalletService.getWallet().save(); } public TradeProtocol getTradeProtocol(Trade trade) { @@ -369,7 +384,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) { tradeProtocol.initialize(processModelServiceProvider, this, trade.getOffer()); - trade.initialize(processModelServiceProvider); requestPersistence(); // TODO requesting persistence twice with initPersistedTrade() } @@ -470,7 +484,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); - maybeRemoveTrade(trade); + removeTrade(trade); }); requestPersistence(); @@ -555,7 +569,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Maker error during trade initialization: " + errorMessage); openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig - maybeRemoveTrade(trade); + removeTrade(trade); if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); @@ -658,45 +672,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer); } - private void handlePaymentAccountKeyRequest(PaymentAccountKeyRequest request, NodeAddress peer) { - log.info("Received PaymentAccountKeyRequest from {} with tradeId {} and uid {}", peer, request.getTradeId(), request.getUid()); - - try { - Validator.nonEmptyStringOf(request.getTradeId()); - } catch (Throwable t) { - log.warn("Invalid PaymentAccountKeyRequest message " + request.toString()); - return; - } - - Optional tradeOptional = getOpenTrade(request.getTradeId()); - if (!tradeOptional.isPresent()) { - log.warn("No trade with id " + request.getTradeId()); - return; - } - Trade trade = tradeOptional.get(); - ((ArbitratorProtocol) getTradeProtocol(trade)).handlePaymentAccountKeyRequest(request, peer); - } - - private void handleUpdateMultisigRequest(UpdateMultisigRequest request, NodeAddress peer) { - log.info("Received UpdateMultisigRequest from {} with tradeId {} and uid {}", peer, request.getTradeId(), request.getUid()); - - try { - Validator.nonEmptyStringOf(request.getTradeId()); - } catch (Throwable t) { - log.warn("Invalid UpdateMultisigRequest message " + request.toString()); - return; - } - - Optional tradeOptional = getOpenTrade(request.getTradeId()); - if (!tradeOptional.isPresent()) throw new RuntimeException("No trade with id " + request.getTradeId()); // TODO (woodser): error handling - Trade trade = tradeOptional.get(); - getTradeProtocol(trade).handleUpdateMultisigRequest(request, peer, errorMessage -> { - log.warn("Error handling UpdateMultisigRequest: " + errorMessage); - if (takeOfferRequestErrorMessageHandler != null) - takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); - }); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Take offer /////////////////////////////////////////////////////////////////////////////////////////// @@ -777,7 +752,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi }, errorMessage -> { log.warn("Taker error during trade initialization: " + errorMessage); errorMessageHandler.handleErrorMessage(errorMessage); - maybeRemoveTrade(trade); + removeTrade(trade); }); requestPersistence(); } @@ -830,8 +805,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades public void onTradeCompleted(Trade trade) { closedTradableManager.add(trade); - trade.setState(Trade.State.WITHDRAW_COMPLETED); - maybeRemoveTrade(trade); + trade.setState(Trade.State.TRADE_COMPLETED); + removeTrade(trade); // TODO The address entry should have been removed already. Check and if its the case remove that. xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId()); @@ -899,7 +874,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published) // we move the trade to failedTradesManager public void onMoveInvalidTradeToFailedTrades(Trade trade) { - maybeRemoveTrade(trade); + removeTrade(trade); failedTradesManager.add(trade); } @@ -1050,34 +1025,28 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); } - private synchronized void maybeRemoveTrade(Trade trade) { - log.info("TradeManager.maybeRemoveTrade()"); + private synchronized void removeTrade(Trade trade) { + log.info("TradeManager.removeTrade()"); synchronized(tradableList) { if (!tradableList.contains(trade)) return; - // delete trade if not possibly funded - if (trade.getPhase().ordinal() < Trade.Phase.DEPOSIT_REQUESTED.ordinal() || trade.getPhase().ordinal() >= Trade.Phase.PAYOUT_PUBLISHED.ordinal()) { // TODO: delete after payout unlocked + // remove trade + tradableList.remove(trade); - // remove trade - tradableList.remove(trade); - - // unreserve trade key images - if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) { - for (String keyImage : trade.getSelf().getReserveTxKeyImages()) { - xmrWalletService.getWallet().thawOutput(keyImage); - } - } - - // delete multisig wallet - deleteTradeWallet(trade); - - // unregister and persist - p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); - requestPersistence(); - } else { - log.warn("Not deleting trade " + trade.getId() + " because its trade wallet might be funded"); - // TODO: schedule wallet for deletion after unlock + // unreserve trade key images + if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) { + for (String keyImage : trade.getSelf().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(keyImage); + xmrWalletService.getWallet().save(); } + + // delete trade wallet if before funded + if (xmrWalletService.multisigWalletExists(trade.getId()) && !trade.isDepositRequested()) { + trade.deleteWallet(); + } + + // unregister and persist + p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); + requestPersistence(); } } @@ -1094,9 +1063,4 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void onTradesChanged() { this.numPendingTrades.set(getObservableList().size()); } - - private void deleteTradeWallet(Trade trade) { - if (xmrWalletService.multisigWalletExists(trade.getId())) xmrWalletService.deleteMultisigWallet(trade.getId()); - else log.warn("Multisig wallet to delete for trade {} does not exist", trade.getId()); - } } diff --git a/core/src/main/java/bisq/core/trade/messages/DepositResponse.java b/core/src/main/java/bisq/core/trade/messages/DepositResponse.java index 9a6ee03b0b..9bc3784e77 100644 --- a/core/src/main/java/bisq/core/trade/messages/DepositResponse.java +++ b/core/src/main/java/bisq/core/trade/messages/DepositResponse.java @@ -21,7 +21,6 @@ import bisq.core.proto.CoreProtoResolver; import bisq.network.p2p.DirectMessage; import bisq.network.p2p.NodeAddress; -import com.google.protobuf.ByteString; import bisq.common.crypto.PubKeyRing; import lombok.EqualsAndHashCode; diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentAccountKeyResponse.java b/core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java similarity index 69% rename from core/src/main/java/bisq/core/trade/messages/PaymentAccountKeyResponse.java rename to core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java index 25bee5abe0..e3c313aae8 100644 --- a/core/src/main/java/bisq/core/trade/messages/PaymentAccountKeyResponse.java +++ b/core/src/main/java/bisq/core/trade/messages/DepositsConfirmedMessage.java @@ -24,6 +24,8 @@ import bisq.network.p2p.NodeAddress; import com.google.protobuf.ByteString; import java.util.Optional; import javax.annotation.Nullable; + +import bisq.common.app.Version; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import lombok.EqualsAndHashCode; @@ -31,25 +33,24 @@ import lombok.Value; @EqualsAndHashCode(callSuper = true) @Value -public final class PaymentAccountKeyResponse extends TradeMailboxMessage implements DirectMessage { +public final class DepositsConfirmedMessage extends TradeMailboxMessage implements DirectMessage { private final NodeAddress senderNodeAddress; private final PubKeyRing pubKeyRing; @Nullable - private final byte[] paymentAccountKey; + private final byte[] sellerPaymentAccountKey; @Nullable private final String updatedMultisigHex; - public PaymentAccountKeyResponse(String tradeId, + public DepositsConfirmedMessage(String tradeId, NodeAddress senderNodeAddress, PubKeyRing pubKeyRing, String uid, - String messageVersion, - @Nullable byte[] paymentAccountKey, + @Nullable byte[] sellerPaymentAccountKey, @Nullable String updatedMultisigHex) { - super(messageVersion, tradeId, uid); + super(Version.getP2PMessageVersion(), tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.pubKeyRing = pubKeyRing; - this.paymentAccountKey = paymentAccountKey; + this.sellerPaymentAccountKey = sellerPaymentAccountKey; this.updatedMultisigHex = updatedMultisigHex; } @@ -60,34 +61,34 @@ public final class PaymentAccountKeyResponse extends TradeMailboxMessage impleme @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.PaymentAccountKeyResponse.Builder builder = protobuf.PaymentAccountKeyResponse.newBuilder() + protobuf.DepositsConfirmedMessage.Builder builder = protobuf.DepositsConfirmedMessage.newBuilder() .setTradeId(tradeId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage()) .setUid(uid); - Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); + Optional.ofNullable(sellerPaymentAccountKey).ifPresent(e -> builder.setSellerPaymentAccountKey(ByteString.copyFrom(e))); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); - return getNetworkEnvelopeBuilder().setPaymentAccountKeyResponse(builder).build(); + return getNetworkEnvelopeBuilder().setDepositsConfirmedMessage(builder).build(); } - public static PaymentAccountKeyResponse fromProto(protobuf.PaymentAccountKeyResponse proto, + public static DepositsConfirmedMessage fromProto(protobuf.DepositsConfirmedMessage proto, CoreProtoResolver coreProtoResolver, String messageVersion) { - return new PaymentAccountKeyResponse(proto.getTradeId(), + return new DepositsConfirmedMessage(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), proto.getUid(), - messageVersion, - ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()), + ProtoUtil.byteArrayOrNullFromProto(proto.getSellerPaymentAccountKey()), ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); } @Override public String toString() { - return "PaymentAccountKeyResponse {" + + return "DepositsConfirmedMessage {" + "\n senderNodeAddress=" + senderNodeAddress + ",\n pubKeyRing=" + pubKeyRing + - ",\n paymentAccountKey=" + paymentAccountKey + + ",\n sellerPaymentAccountKey=" + sellerPaymentAccountKey + + ",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentAccountKeyRequest.java b/core/src/main/java/bisq/core/trade/messages/PaymentAccountKeyRequest.java deleted file mode 100644 index 81186e8a00..0000000000 --- a/core/src/main/java/bisq/core/trade/messages/PaymentAccountKeyRequest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.messages; - -import bisq.core.proto.CoreProtoResolver; - -import bisq.network.p2p.DirectMessage; -import bisq.network.p2p.NodeAddress; -import bisq.common.crypto.PubKeyRing; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -@EqualsAndHashCode(callSuper = true) -@Value -public final class PaymentAccountKeyRequest extends TradeMessage implements DirectMessage { - private final NodeAddress senderNodeAddress; - private final PubKeyRing pubKeyRing; - - public PaymentAccountKeyRequest(String tradeId, - NodeAddress senderNodeAddress, - PubKeyRing pubKeyRing, - String uid, - String messageVersion) { - super(messageVersion, tradeId, uid); - this.senderNodeAddress = senderNodeAddress; - this.pubKeyRing = pubKeyRing; - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.PaymentAccountKeyRequest.Builder builder = protobuf.PaymentAccountKeyRequest.newBuilder() - .setTradeId(tradeId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setUid(uid); - return getNetworkEnvelopeBuilder().setPaymentAccountKeyRequest(builder).build(); - } - - public static PaymentAccountKeyRequest fromProto(protobuf.PaymentAccountKeyRequest proto, - CoreProtoResolver coreProtoResolver, - String messageVersion) { - return new PaymentAccountKeyRequest(proto.getTradeId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - PubKeyRing.fromProto(proto.getPubKeyRing()), - proto.getUid(), - messageVersion); - } - - @Override - public String toString() { - return "PaymentAccountKeyRequest {" + - "\n senderNodeAddress=" + senderNodeAddress + - ",\n pubKeyRing=" + pubKeyRing + - "\n} " + super.toString(); - } -} diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java index e392e303fb..57c80594f9 100644 --- a/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java @@ -22,6 +22,7 @@ import bisq.core.account.sign.SignedWitness; import bisq.network.p2p.NodeAddress; import bisq.common.app.Version; +import bisq.common.proto.ProtoUtil; import bisq.common.proto.network.NetworkEnvelope; import java.util.Optional; @@ -38,7 +39,12 @@ import javax.annotation.Nullable; @Value public final class PaymentReceivedMessage extends TradeMailboxMessage { private final NodeAddress senderNodeAddress; - private final String payoutTxHex; + @Nullable + private final String unsignedPayoutTxHex; + @Nullable + private final String signedPayoutTxHex; + private final String updatedMultisigHex; + private final boolean sawArrivedPaymentReceivedMsg; // Added in v1.4.0 @Nullable @@ -47,13 +53,19 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { public PaymentReceivedMessage(String tradeId, NodeAddress senderNodeAddress, @Nullable SignedWitness signedWitness, - String signedPayoutTxHex) { + String unsignedPayoutTxHex, + String signedPayoutTxHex, + String updatedMultisigHex, + boolean sawArrivedPaymentReceivedMsg) { this(tradeId, senderNodeAddress, signedWitness, UUID.randomUUID().toString(), Version.getP2PMessageVersion(), - signedPayoutTxHex); + unsignedPayoutTxHex, + signedPayoutTxHex, + updatedMultisigHex, + sawArrivedPaymentReceivedMsg); } @@ -66,11 +78,17 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { @Nullable SignedWitness signedWitness, String uid, String messageVersion, - String signedPayoutTxHex) { + String unsignedPayoutTxHex, + String signedPayoutTxHex, + String updatedMultisigHex, + boolean sawArrivedPaymentReceivedMsg) { super(messageVersion, tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.signedWitness = signedWitness; - this.payoutTxHex = signedPayoutTxHex; + this.unsignedPayoutTxHex = unsignedPayoutTxHex; + this.signedPayoutTxHex = signedPayoutTxHex; + this.updatedMultisigHex = updatedMultisigHex; + this.sawArrivedPaymentReceivedMsg = sawArrivedPaymentReceivedMsg; } @Override @@ -79,8 +97,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { .setTradeId(tradeId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setUid(uid) - .setPayoutTxHex(payoutTxHex); + .setSawArrivedPaymentReceivedMsg(sawArrivedPaymentReceivedMsg); Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); + Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); + Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex)); + Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex)); return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build(); } @@ -96,7 +117,10 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { signedWitness, proto.getUid(), messageVersion, - proto.getPayoutTxHex()); + ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()), + ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()), + ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), + proto.getSawArrivedPaymentReceivedMsg()); } @Override @@ -104,7 +128,10 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage { return "SellerReceivedPaymentMessage{" + "\n senderNodeAddress=" + senderNodeAddress + ",\n signedWitness=" + signedWitness + - ",\n payoutTxHex=" + payoutTxHex + + ",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex + + ",\n signedPayoutTxHex=" + signedPayoutTxHex + + ",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) + + ",\n sawArrivedPaymentReceivedMsg=" + sawArrivedPaymentReceivedMsg + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java b/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java index 1342237c1f..b482a4aa20 100644 --- a/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java @@ -52,8 +52,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage { @Nullable String counterCurrencyTxId, @Nullable String counterCurrencyExtraData, String uid, - String signedPayoutTxHex, - String updatedMultisigHex, + @Nullable String signedPayoutTxHex, + @Nullable String updatedMultisigHex, @Nullable byte[] paymentAccountKey) { this(tradeId, senderNodeAddress, diff --git a/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java deleted file mode 100644 index 69a7602767..0000000000 --- a/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.messages; - -import bisq.core.account.sign.SignedWitness; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.app.Version; -import bisq.common.proto.network.NetworkEnvelope; - -import java.util.Optional; -import java.util.UUID; - -import lombok.EqualsAndHashCode; -import lombok.Value; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nullable; - -@Slf4j -@EqualsAndHashCode(callSuper = true) -@Value -public final class PayoutTxPublishedMessage extends TradeMailboxMessage { - private final NodeAddress senderNodeAddress; - private final boolean isMaker; - private final String signedPayoutTxHex; - - // Added in v1.4.0 - @Nullable - private final SignedWitness signedWitness; - - public PayoutTxPublishedMessage(String tradeId, - NodeAddress senderNodeAddress, - boolean isMaker, - @Nullable SignedWitness signedWitness, - String signedPayoutTxHex) { - this(tradeId, - senderNodeAddress, - isMaker, - signedWitness, - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - signedPayoutTxHex); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - private PayoutTxPublishedMessage(String tradeId, - NodeAddress senderNodeAddress, - boolean isMaker, - @Nullable SignedWitness signedWitness, - String uid, - String messageVersion, - String signedPayoutTxHex) { - super(messageVersion, tradeId, uid); - this.senderNodeAddress = senderNodeAddress; - this.isMaker = isMaker; - this.signedWitness = signedWitness; - this.signedPayoutTxHex = signedPayoutTxHex; - } - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.PayoutTxPublishedMessage.Builder builder = protobuf.PayoutTxPublishedMessage.newBuilder() - .setTradeId(tradeId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setIsMaker(isMaker) - .setUid(uid) - .setSignedPayoutTxHex(signedPayoutTxHex); - Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); - return getNetworkEnvelopeBuilder().setPayoutTxPublishedMessage(builder).build(); - } - - public static NetworkEnvelope fromProto(protobuf.PayoutTxPublishedMessage proto, String messageVersion) { - // There is no method to check for a nullable non-primitive data type object but we know that all fields - // are empty/null, so we check for the signature to see if we got a valid signedWitness. - protobuf.SignedWitness protoSignedWitness = proto.getSignedWitness(); - SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ? - SignedWitness.fromProto(protoSignedWitness) : - null; - return new PayoutTxPublishedMessage(proto.getTradeId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getIsMaker(), - signedWitness, - proto.getUid(), - messageVersion, - proto.getSignedPayoutTxHex()); - } - - @Override - public String toString() { - return "PayoutTxPublishedMessage{" + - "\n senderNodeAddress=" + senderNodeAddress + - ",\n isMaker=" + isMaker + - ",\n signedWitness=" + signedWitness + - ",\n signedPayoutTxHex=" + signedPayoutTxHex + - "\n} " + super.toString(); - } -} diff --git a/core/src/main/java/bisq/core/trade/messages/UpdateMultisigRequest.java b/core/src/main/java/bisq/core/trade/messages/UpdateMultisigRequest.java deleted file mode 100644 index b053f8d6e0..0000000000 --- a/core/src/main/java/bisq/core/trade/messages/UpdateMultisigRequest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.messages; - -import bisq.core.proto.CoreProtoResolver; - -import bisq.network.p2p.DirectMessage; -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; -import bisq.common.proto.ProtoUtil; - -import java.util.Optional; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -import javax.annotation.Nullable; - -@EqualsAndHashCode(callSuper = true) -@Value -public final class UpdateMultisigRequest extends TradeMessage implements DirectMessage { - private final NodeAddress senderNodeAddress; - private final PubKeyRing pubKeyRing; - private final long currentDate; - @Nullable - private final String updatedMultisigHex; - - public UpdateMultisigRequest(String tradeId, - NodeAddress senderNodeAddress, - PubKeyRing pubKeyRing, - String uid, - String messageVersion, - long currentDate, - String updatedMultisigHex) { - super(messageVersion, tradeId, uid); - this.senderNodeAddress = senderNodeAddress; - this.pubKeyRing = pubKeyRing; - this.currentDate = currentDate; - this.updatedMultisigHex = updatedMultisigHex; - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.UpdateMultisigRequest.Builder builder = protobuf.UpdateMultisigRequest.newBuilder() - .setTradeId(tradeId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setUid(uid); - - Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); - - builder.setCurrentDate(currentDate); - - return getNetworkEnvelopeBuilder().setUpdateMultisigRequest(builder).build(); - } - - public static UpdateMultisigRequest fromProto(protobuf.UpdateMultisigRequest proto, - CoreProtoResolver coreProtoResolver, - String messageVersion) { - return new UpdateMultisigRequest(proto.getTradeId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - PubKeyRing.fromProto(proto.getPubKeyRing()), - proto.getUid(), - messageVersion, - proto.getCurrentDate(), - ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); - } - - @Override - public String toString() { - return "UpdateMultisigRequest {" + - "\n senderNodeAddress=" + senderNodeAddress + - ",\n pubKeyRing=" + pubKeyRing + - ",\n currentDate=" + currentDate + - ",\n updatedMultisigHex='" + updatedMultisigHex + - "\n} " + super.toString(); - } -} diff --git a/core/src/main/java/bisq/core/trade/messages/UpdateMultisigResponse.java b/core/src/main/java/bisq/core/trade/messages/UpdateMultisigResponse.java deleted file mode 100644 index 830c3ec5f3..0000000000 --- a/core/src/main/java/bisq/core/trade/messages/UpdateMultisigResponse.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.messages; - -import bisq.core.proto.CoreProtoResolver; - -import bisq.network.p2p.DirectMessage; -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; -import bisq.common.proto.ProtoUtil; - -import java.util.Optional; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -import javax.annotation.Nullable; - -@EqualsAndHashCode(callSuper = true) -@Value -public final class UpdateMultisigResponse extends TradeMessage implements DirectMessage { - private final NodeAddress senderNodeAddress; - private final PubKeyRing pubKeyRing; - private final long currentDate; - @Nullable - private final String updatedMultisigHex; - - public UpdateMultisigResponse(String tradeId, - NodeAddress senderNodeAddress, - PubKeyRing pubKeyRing, - String uid, - String messageVersion, - long currentDate, - String updatedMultisigHex) { - super(messageVersion, tradeId, uid); - this.senderNodeAddress = senderNodeAddress; - this.pubKeyRing = pubKeyRing; - this.currentDate = currentDate; - this.updatedMultisigHex = updatedMultisigHex; - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.UpdateMultisigResponse.Builder builder = protobuf.UpdateMultisigResponse.newBuilder() - .setTradeId(tradeId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setUid(uid); - - Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); - - builder.setCurrentDate(currentDate); - - return getNetworkEnvelopeBuilder().setUpdateMultisigResponse(builder).build(); - } - - public static UpdateMultisigResponse fromProto(protobuf.UpdateMultisigResponse proto, - CoreProtoResolver coreProtoResolver, - String messageVersion) { - return new UpdateMultisigResponse(proto.getTradeId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - PubKeyRing.fromProto(proto.getPubKeyRing()), - proto.getUid(), - messageVersion, - proto.getCurrentDate(), - ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); - } - - @Override - public String toString() { - return "UpdateMultisigResponse {" + - "\n senderNodeAddress=" + senderNodeAddress + - ",\n pubKeyRing=" + pubKeyRing + - ",\n currentDate=" + currentDate + - ",\n updatedMultisigHex='" + updatedMultisigHex + - "\n} " + super.toString(); - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java index d28e792579..9bdc8fa952 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/ArbitratorProtocol.java @@ -6,18 +6,16 @@ import bisq.core.trade.Trade; import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.messages.PaymentAccountKeyRequest; import bisq.core.trade.messages.SignContractResponse; -import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.TradeMessage; -import bisq.core.trade.protocol.FluentProtocol.Condition; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ArbitratorProcessDepositRequest; -import bisq.core.trade.protocol.tasks.ArbitratorProcessPaymentAccountKeyRequest; import bisq.core.trade.protocol.tasks.ArbitratorProcessReserveTx; -import bisq.core.trade.protocol.tasks.ArbitratorProcessPayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.ArbitratorSendInitTradeOrMultisigRequests; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; +import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer; +import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller; +import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.util.Validator; import bisq.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @@ -32,17 +30,11 @@ public class ArbitratorProtocol extends DisputeProtocol { @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); - if (message instanceof PayoutTxPublishedMessage) { - handle((PayoutTxPublishedMessage) message, peer); - } } @Override public void onMailboxMessage(TradeMessage message, NodeAddress peer) { super.onMailboxMessage(message, peer); - if (message instanceof PayoutTxPublishedMessage) { - handle((PayoutTxPublishedMessage) message, peer); - } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -118,57 +110,10 @@ public class ArbitratorProtocol extends DisputeProtocol { public void handleDepositResponse(DepositResponse response, NodeAddress sender) { log.warn("Arbitrator ignoring DepositResponse for trade " + response.getTradeId()); } - - public void handlePaymentAccountKeyRequest(PaymentAccountKeyRequest request, NodeAddress sender) { - System.out.println("ArbitratorProtocol.handlePaymentAccountKeyRequest() " + trade.getId()); - new Thread(() -> { - synchronized (trade) { - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(new Condition(trade) - .with(request) - .from(sender)) - .setup(tasks( - ArbitratorProcessPaymentAccountKeyRequest.class) - .using(new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(sender, request); - }, - errorMessage -> { - handleTaskRunnerFault(sender, request, errorMessage); - })) - .withTimeout(TRADE_TIMEOUT)) - .executeTasks(true); - awaitTradeLatch(); - } - }).start(); - } - - protected void handle(PayoutTxPublishedMessage request, NodeAddress peer) { - System.out.println("ArbitratorProtocol.handle(PayoutTxPublishedMessage)"); - new Thread(() -> { - synchronized (trade) { - if (trade.isCompleted()) return; // ignore subsequent requests - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), request); - processModel.setTradeMessage(request); - expect(anyPhase(Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED) - .with(request) - .from(peer)) - .setup(tasks( - ArbitratorProcessPayoutTxPublishedMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, request); - }, - errorMessage -> { - handleTaskRunnerFault(peer, request, errorMessage); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }).start(); + + @SuppressWarnings("unchecked") + @Override + public Class[] getDepsitsConfirmedTasks() { + return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class }; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index 8f0ca1e999..fbd8a0c11d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -22,7 +22,7 @@ import bisq.core.trade.Trade; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.messages.PaymentAccountKeyResponse; +import bisq.core.trade.messages.DepositsConfirmedMessage; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; @@ -31,7 +31,7 @@ import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; - +import bisq.common.taskrunner.Task; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -102,7 +102,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol } @Override - public void handle(PaymentAccountKeyResponse request, NodeAddress sender) { + public void handle(DepositsConfirmedMessage request, NodeAddress sender) { super.handle(request, sender); } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index 371cdaf416..a23fd1bf78 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -24,7 +24,7 @@ import bisq.core.trade.Trade; import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.InitMultisigRequest; -import bisq.core.trade.messages.PaymentAccountKeyResponse; +import bisq.core.trade.messages.DepositsConfirmedMessage; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; @@ -113,7 +113,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol } @Override - public void handle(PaymentAccountKeyResponse request, NodeAddress sender) { + public void handle(DepositsConfirmedMessage request, NodeAddress sender) { super.handle(request, sender); } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java index 970fdce233..fe900e5d31 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -17,35 +17,23 @@ package bisq.core.trade.protocol; -import bisq.common.UserThread; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import bisq.core.trade.BuyerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PaymentAccountKeyResponse; -import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; -import bisq.core.trade.protocol.FluentProtocol.Condition; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; -import bisq.core.trade.protocol.tasks.BuyerProcessPaymentAccountKeyResponse; -import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage; -import bisq.core.trade.protocol.tasks.BuyerSendPaymentAccountKeyRequestToArbitrator; import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; -import bisq.core.trade.protocol.tasks.BuyerSendPayoutTxPublishedMessage; -import bisq.core.trade.protocol.tasks.SetupDepositTxsListener; -import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; -import bisq.core.util.Validator; +import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; +import bisq.core.trade.protocol.tasks.TradeTask; import bisq.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; -import org.fxmisc.easybind.EasyBind; @Slf4j -public abstract class BuyerProtocol extends DisputeProtocol { +public class BuyerProtocol extends DisputeProtocol { - private boolean listeningToSendPaymentAccountKey; - private boolean paymentAccountPayloadKeyRequestSent; enum BuyerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -66,23 +54,8 @@ public abstract class BuyerProtocol extends DisputeProtocol { // TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades - // request key to decrypt seller's payment account payload after first confirmation - sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent.STARTUP, false); - - // listen for deposit txs - given(anyPhase(Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED) - .with(BuyerEvent.STARTUP)) - .setup(tasks(SetupDepositTxsListener.class)) - .executeTasks(); - - // listen for payout tx - given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) - .with(BuyerEvent.STARTUP)) - .setup(tasks(SetupPayoutTxListener.class)) - .executeTasks(); - // send payment sent message - given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) // TODO: remove payment received phase? + given(anyPhase(Trade.Phase.PAYMENT_SENT) .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) .with(BuyerEvent.STARTUP)) .setup(tasks(BuyerSendPaymentSentMessage.class)) @@ -92,49 +65,16 @@ public abstract class BuyerProtocol extends DisputeProtocol { @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); - if (message instanceof PaymentReceivedMessage) { - handle((PaymentReceivedMessage) message, peer); - } if (message instanceof PaymentAccountKeyResponse) { - handle((PaymentAccountKeyResponse) message, peer); - } } @Override public void onMailboxMessage(TradeMessage message, NodeAddress peer) { super.onMailboxMessage(message, peer); - if (message instanceof PaymentReceivedMessage) { - handle((PaymentReceivedMessage) message, peer); - } else if (message instanceof PaymentAccountKeyResponse) { - handle((PaymentAccountKeyResponse) message, peer); - } } @Override public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) { super.handleSignContractResponse(response, sender); - sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent.DEPOSIT_TXS_CONFIRMED, true); - } - - public void handle(PaymentAccountKeyResponse response, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountKeyResponse()"); - new Thread(() -> { - synchronized (trade) { - latchTrade(); - expect(new Condition(trade) - .with(response) - .from(sender)) - .setup(tasks(BuyerProcessPaymentAccountKeyResponse.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(sender, response); - }, - errorMessage -> { - handleTaskRunnerFault(sender, response, errorMessage); - }))) - .executeTasks(); - awaitTradeLatch(); - } - }).start(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -177,84 +117,9 @@ public abstract class BuyerProtocol extends DisputeProtocol { }).start(); } - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message Payout tx - /////////////////////////////////////////////////////////////////////////////////////////// - - protected void handle(PaymentReceivedMessage message, NodeAddress peer) { - System.out.println("BuyerProtocol.handle(PaymentReceivedMessage)"); - new Thread(() -> { - synchronized (trade) { - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) - .with(message) - .from(peer)) - .setup(tasks( - BuyerProcessPaymentReceivedMessage.class, - BuyerSendPayoutTxPublishedMessage.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(peer, message); - }, - errorMessage -> { - handleTaskRunnerFault(peer, message, errorMessage); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }).start(); - } - - private void sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent event, boolean waitForSellerOnConfirm) { - - // skip if payment account payload already decrypted or not enough progress - if (trade.getSeller().getPaymentAccountPayload() != null) return; - if (trade.getPhase().ordinal() < Trade.Phase.DEPOSIT_REQUESTED.ordinal()) return; - - // if confirmed and waiting for seller, recheck later - if (trade.getState() == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN && waitForSellerOnConfirm) { - UserThread.runAfter(() -> { - sendPaymentAccountKeyRequestIfWhenNeeded(event, false); - }, TRADE_TIMEOUT); - return; - } - - // else if confirmed send request and return - else if (trade.getState().ordinal() >= Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN.ordinal()) { - sendPaymentAccountKeyRequest(event); - return; - } - - // register for state changes once - if (!listeningToSendPaymentAccountKey) { - listeningToSendPaymentAccountKey = true; - EasyBind.subscribe(trade.stateProperty(), state -> { - sendPaymentAccountKeyRequestIfWhenNeeded(event, waitForSellerOnConfirm); - }); - } - } - - private void sendPaymentAccountKeyRequest(BuyerEvent event) { - new Thread(() -> { - synchronized (trade) { - if (paymentAccountPayloadKeyRequestSent) return; - if (trade.getSeller().getPaymentAccountPayload() != null) return; // skip if initialized - latchTrade(); - expect(new Condition(trade)) - .setup(tasks(BuyerSendPaymentAccountKeyRequestToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(event); - }, - (errorMessage) -> { - handleTaskRunnerFault(event, errorMessage); - }))) - .executeTasks(true); - awaitTradeLatch(); - paymentAccountPayloadKeyRequestSent = true; - } - }).start(); + @SuppressWarnings("unchecked") + @Override + public Class[] getDepsitsConfirmedTasks() { + return new Class[] { SendDepositsConfirmedMessageToArbitrator.class }; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java index e6aa2563cc..e0104b12ba 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java @@ -22,25 +22,20 @@ import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; -import bisq.core.trade.protocol.BuyerProtocol.BuyerEvent; -import bisq.core.trade.protocol.FluentProtocol.Condition; import bisq.core.trade.protocol.tasks.ApplyFilter; -import bisq.core.trade.protocol.tasks.SellerMaybeSendPayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage; -import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage; -import bisq.core.trade.protocol.tasks.SellerSendPaymentAccountPayloadKey; -import bisq.core.trade.protocol.tasks.SetupDepositTxsListener; -import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; +import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator; +import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer; +import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; +import bisq.core.trade.protocol.tasks.TradeTask; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; - import lombok.extern.slf4j.Slf4j; -import org.fxmisc.easybind.EasyBind; @Slf4j -public abstract class SellerProtocol extends DisputeProtocol { +public class SellerProtocol extends DisputeProtocol { enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -54,25 +49,6 @@ public abstract class SellerProtocol extends DisputeProtocol { @Override protected void onInitialized() { super.onInitialized(); - - // TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades - - // send payment account payload key when trade state is confirmed - if (trade.getPhase() == Trade.Phase.DEPOSIT_REQUESTED || trade.getPhase() == Trade.Phase.DEPOSITS_PUBLISHED) { - sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent.STARTUP); - } - - // listen for deposit txs - given(anyPhase(Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED) - .with(SellerEvent.STARTUP)) - .setup(tasks(SetupDepositTxsListener.class)) - .executeTasks(); - - // listen for payout tx - given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) - .with(BuyerEvent.STARTUP)) - .setup(tasks(SetupPayoutTxListener.class)) - .executeTasks(); } @Override @@ -94,7 +70,6 @@ public abstract class SellerProtocol extends DisputeProtocol { @Override public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) { - sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent.DEPOSIT_TXS_CONFIRMED); super.handleSignContractResponse(response, sender); } @@ -163,8 +138,8 @@ public abstract class SellerProtocol extends DisputeProtocol { .setup(tasks( ApplyFilter.class, SellerPreparePaymentReceivedMessage.class, - SellerMaybeSendPayoutTxPublishedMessage.class, - SellerSendPaymentReceivedMessage.class) + SellerSendPaymentReceivedMessageToBuyer.class, + SellerSendPaymentReceivedMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { this.errorMessageHandler = null; handleTaskRunnerSuccess(event); @@ -183,26 +158,9 @@ public abstract class SellerProtocol extends DisputeProtocol { }).start(); } - private void sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent event) { - EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) { - new Thread(() -> { - synchronized (trade) { - latchTrade(); - expect(new Condition(trade)) - .setup(tasks(SellerSendPaymentAccountPayloadKey.class) - .using(new TradeTaskRunner(trade, - () -> { - handleTaskRunnerSuccess(event); - }, - (errorMessage) -> { - handleTaskRunnerFault(event, errorMessage); - }))) - .executeTasks(true); - awaitTradeLatch(); - } - }).start(); - } - }); + @SuppressWarnings("unchecked") + @Override + public Class[] getDepsitsConfirmedTasks() { + return new Class[] { SendDepositsConfirmedMessageToBuyer.class }; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index 5fe4ca92c0..fe798b6778 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -18,24 +18,30 @@ package bisq.core.trade.protocol; import bisq.core.offer.Offer; +import bisq.core.trade.ArbitratorTrade; +import bisq.core.trade.BuyerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.DepositResponse; +import bisq.core.trade.messages.DepositsConfirmedMessage; import bisq.core.trade.messages.InitMultisigRequest; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; -import bisq.core.trade.messages.UpdateMultisigRequest; import bisq.core.trade.protocol.tasks.RemoveOffer; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.FluentProtocol.Condition; import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest; import bisq.core.trade.protocol.tasks.ProcessDepositResponse; +import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage; import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest; +import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import bisq.core.trade.protocol.tasks.ProcessSignContractRequest; import bisq.core.trade.protocol.tasks.ProcessSignContractResponse; -import bisq.core.trade.protocol.tasks.ProcessUpdateMultisigRequest; import bisq.core.util.Validator; import bisq.network.p2p.AckMessage; @@ -47,7 +53,6 @@ import bisq.network.p2p.SendMailboxMessageListener; import bisq.network.p2p.mailbox.MailboxMessage; import bisq.network.p2p.mailbox.MailboxMessageService; import bisq.network.p2p.messaging.DecryptedMailboxListener; - import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; @@ -58,7 +63,6 @@ import bisq.common.taskrunner.Task; import java.util.Collection; import java.util.Collections; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.fxmisc.easybind.EasyBind; @@ -93,10 +97,20 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); + if (message instanceof DepositsConfirmedMessage) { + handle((DepositsConfirmedMessage) message, peerNodeAddress); + } else if (message instanceof PaymentReceivedMessage) { + handle((PaymentReceivedMessage) message, peerNodeAddress); + } } protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); + if (message instanceof DepositsConfirmedMessage) { + handle((DepositsConfirmedMessage) message, peerNodeAddress); + } else if (message instanceof PaymentReceivedMessage) { + handle((PaymentReceivedMessage) message, peerNodeAddress); + } } @@ -110,20 +124,22 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } protected void onInitialized() { - if (!trade.isWithdrawn()) { + if (!trade.isCompleted()) { processModel.getP2PService().addDecryptedDirectMessageListener(this); } + // handle trade events + EasyBind.subscribe(trade.stateProperty(), state -> { + if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage(); + }); + + // initialize trade + trade.initialize(processModel.getProvider()); + + // process mailbox messages MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); - // We delay a bit here as the trade gets updated from the wallet to update the trade - // state (deposit confirmed) and that happens after our method is called. - // TODO To fix that in a better way we would need to change the order of some routines - // from the TradeManager, but as we are close to a release I dont want to risk a bigger - // change and leave that for a later PR - UserThread.runAfter(() -> { - mailboxMessageService.addDecryptedMailboxListener(this); - handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); - }, 100, TimeUnit.MILLISECONDS); + mailboxMessageService.addDecryptedMailboxListener(this); + handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); } public void onWithdrawCompleted() { @@ -196,7 +212,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D TradeMessage tradeMessage = (TradeMessage) mailboxMessage; // We only remove here if we have already completed the trade. // Otherwise removal is done after successfully applied the task runner. - if (trade.isWithdrawn()) { + if (trade.isCompleted()) { processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(mailboxMessage); log.info("Remove {} from the P2P network as trade is already completed.", tradeMessage.getClass().getSimpleName()); @@ -205,7 +221,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); } else if (mailboxMessage instanceof AckMessage) { AckMessage ackMessage = (AckMessage) mailboxMessage; - if (!trade.isWithdrawn()) { + if (!trade.isCompleted()) { // We only apply the msg if we have not already completed the trade onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); } @@ -227,8 +243,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // Abstract /////////////////////////////////////////////////////////////////////////////////////////// + public abstract Class[] getDepsitsConfirmedTasks(); + public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()"); + System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()"); new Thread(() -> { synchronized (trade) { latchTrade(); @@ -256,7 +274,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest() " + trade.getId()); + System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() " + trade.getId()); new Thread(() -> { synchronized (trade) { Validator.checkTradeId(processModel.getOfferId(), message); @@ -292,7 +310,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); + System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() " + trade.getId()); new Thread(() -> { synchronized (trade) { Validator.checkTradeId(processModel.getOfferId(), message); @@ -329,7 +347,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()"); + System.out.println(getClass().getSimpleName() + ".handleDepositResponse()"); new Thread(() -> { synchronized (trade) { latchTrade(); @@ -358,25 +376,55 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }).start(); } - // TODO (woodser): update to use fluent for consistency - public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - latchTrade(); - Validator.checkTradeId(processModel.getOfferId(), message); - processModel.setTradeMessage(message); - TradeTaskRunner taskRunner = new TradeTaskRunner(trade, - () -> { - stopTimeout(); - handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest"); - }, - errorMessage -> { - handleTaskRunnerFault(peer, message, errorMessage); - }); - taskRunner.addTasks( - ProcessUpdateMultisigRequest.class - ); - startTimeout(TRADE_TIMEOUT); - taskRunner.run(); - awaitTradeLatch(); + public void handle(DepositsConfirmedMessage response, NodeAddress sender) { + System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage)"); + new Thread(() -> { + synchronized (trade) { + latchTrade(); + expect(new Condition(trade) + .with(response) + .from(sender)) + .setup(tasks(ProcessDepositsConfirmedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(sender, response); + }, + errorMessage -> { + handleTaskRunnerFault(sender, response, errorMessage); + }))) + .executeTasks(); + awaitTradeLatch(); + } + }).start(); + } + + // received by buyer and arbitrator + protected void handle(PaymentReceivedMessage message, NodeAddress peer) { + System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)"); + if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { + log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); + return; + } + if (trade instanceof ArbitratorTrade && !trade.isPayoutUnlocked()) trade.syncWallet(); // arbitrator syncs slowly after deposits confirmed + synchronized (trade) { + latchTrade(); + Validator.checkTradeId(processModel.getOfferId(), message); + processModel.setTradeMessage(message); + expect(anyPhase(trade instanceof ArbitratorTrade ? new Trade.Phase[] { Trade.Phase.DEPOSITS_UNLOCKED } : new Trade.Phase[] { Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED }) + .with(message) + .from(peer)) + .setup(tasks( + ProcessPaymentReceivedMessage.class) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + handleTaskRunnerFault(peer, message, errorMessage); + }))) + .executeTasks(true); + awaitTradeLatch(); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -591,7 +639,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // Private /////////////////////////////////////////////////////////////////////////////////////////// - private void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) { + protected void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) { log.info("TaskRunner successfully completed. Triggered from {}, tradeId={}", source, trade.getId()); if (message != null) { sendAckMessage(sender, message, true, null); @@ -638,7 +686,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void awaitTradeLatch() { if (tradeLatch == null) return; - TradeUtils.awaitLatch(tradeLatch); + HavenoUtils.awaitLatch(tradeLatch); } private boolean isMyMessage(NetworkEnvelope message) { @@ -653,4 +701,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return false; } } + + private void sendDepositsConfirmedMessage() { + new Thread(() -> { + synchronized (trade) { + latchTrade(); + expect(new Condition(trade)) + .setup(tasks(getDepsitsConfirmedTasks()) + .using(new TradeTaskRunner(trade, + () -> { + handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages"); + }, + (errorMessage) -> { + handleTaskRunnerFault(null, null, errorMessage); + }))) + .executeTasks(true); + awaitTradeLatch(); + } + }).start(); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java index 2261be44c2..531acd3073 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java @@ -36,7 +36,6 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroTx; -import monero.wallet.model.MoneroTxWallet; import javax.annotation.Nullable; // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessPaymentAccountKeyRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessPaymentAccountKeyRequest.java deleted file mode 100644 index da79589f12..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessPaymentAccountKeyRequest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - - -import bisq.common.app.Version; -import bisq.common.crypto.PubKeyRing; -import bisq.common.taskrunner.TaskRunner; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.PaymentAccountKeyResponse; -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.SendDirectMessageListener; -import java.util.UUID; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ArbitratorProcessPaymentAccountKeyRequest extends TradeTask { - - @SuppressWarnings({"unused"}) - public ArbitratorProcessPaymentAccountKeyRequest(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // ensure deposit txs confirmed - trade.listenForDepositTxs(); - if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { - throw new RuntimeException("Arbitrator refusing payment account key request for trade " + trade.getId() + " because the deposit txs have not confirmed"); - } - - // create response for buyer with key to decrypt seller's payment account payload - PaymentAccountKeyResponse response = new PaymentAccountKeyResponse( - trade.getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - trade.getSeller().getPaymentAccountKey(), - null - ); - - // send response to buyer - NodeAddress buyerAddress = trade.getBuyer().getNodeAddress(); - log.info("Arbitrator sending PaymentAccountKeyResponse to buyer={}; offerId={}", buyerAddress, trade.getId()); - processModel.getP2PService().sendEncryptedDirectMessage(buyerAddress, trade.getBuyer().getPubKeyRing(), response, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), buyerAddress, trade.getId()); - complete(); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), buyerAddress, trade.getId(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage); - failed(); - } - }); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessPayoutTxPublishedMessage.java deleted file mode 100644 index bec4cc2a23..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessPayoutTxPublishedMessage.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - - -import bisq.common.taskrunner.TaskRunner; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayoutTxPublishedMessage; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class ArbitratorProcessPayoutTxPublishedMessage extends TradeTask { - - @SuppressWarnings({"unused"}) - public ArbitratorProcessPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - PayoutTxPublishedMessage request = (PayoutTxPublishedMessage) processModel.getTradeMessage(); - - // verify and publish payout tx - trade.verifyPayoutTx(request.getSignedPayoutTxHex(), false, true); - - // update latest peer address - if (request.isMaker()) trade.getMaker().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); - else trade.getTaker().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); - - // TODO: publish signed witness data? - //request.getSignedWitness() - - // close arbitrator trade - processModel.getTradeManager().onTradeCompleted(trade); - complete(); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index ca7b436dba..b6c9f8cee0 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -63,24 +63,25 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); - // create payout tx if we have seller's updated multisig hex - if (trade.getTradingPeer().getUpdatedMultisigHex() != null) { + // import multisig hex + List updatedMultisigHexes = new ArrayList(); + if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex()); + if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex()); + if (!updatedMultisigHexes.isEmpty()) { + multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually + trade.saveWallet(); + } - // create payout tx + // create payout tx if we have seller's updated multisig hex + if (trade.getSeller().getUpdatedMultisigHex() != null) { + + // create payout tx log.info("Buyer creating unsigned payout tx"); - multisigWallet.importMultisigHex(trade.getTradingPeer().getUpdatedMultisigHex()); MoneroTxWallet payoutTx = trade.createPayoutTx(); trade.setPayoutTx(payoutTx); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - - // start listening for published payout tx - trade.listenForPayoutTx(); - } else { - if (trade.getSelf().getUpdatedMultisigHex() == null) trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); // only export multisig hex once } - // close multisig wallet - walletService.closeMultisigWallet(trade.getId()); complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerProcessPaymentAccountKeyResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerProcessPaymentAccountKeyResponse.java deleted file mode 100644 index 08015fcffe..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerProcessPaymentAccountKeyResponse.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - - -import bisq.common.taskrunner.TaskRunner; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.PaymentAccountKeyResponse; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class BuyerProcessPaymentAccountKeyResponse extends TradeTask { - - @SuppressWarnings({"unused"}) - public BuyerProcessPaymentAccountKeyResponse(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // update peer node address if not from arbitrator - if (!processModel.getTempTradingPeerNodeAddress().equals(trade.getArbitrator().getNodeAddress())) { - trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); - } - - // decrypt peer's payment account payload - PaymentAccountKeyResponse request = (PaymentAccountKeyResponse) processModel.getTradeMessage(); - if (trade.getTradingPeer().getPaymentAccountPayload() == null) { - trade.decryptPeersPaymentAccountPayload(request.getPaymentAccountKey()); - } - - // store updated multisig hex for processing on payment sent - if (request.getUpdatedMultisigHex() != null) trade.getTradingPeer().setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - - // persist and complete - processModel.getTradeManager().requestPersistence(); - complete(); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentAccountKeyRequestToArbitrator.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentAccountKeyRequestToArbitrator.java deleted file mode 100644 index 15f3ee161f..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentAccountKeyRequestToArbitrator.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - -import bisq.common.app.Version; -import bisq.common.taskrunner.TaskRunner; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.messages.PaymentAccountKeyRequest; -import bisq.network.p2p.SendDirectMessageListener; -import java.util.UUID; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class BuyerSendPaymentAccountKeyRequestToArbitrator extends TradeTask { - - @SuppressWarnings({"unused"}) - public BuyerSendPaymentAccountKeyRequestToArbitrator(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // create request to arbitrator - PaymentAccountKeyRequest request = new PaymentAccountKeyRequest( - trade.getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion() - ); - - // send request to arbitrator - log.info("Sending {} with offerId {} and uid {} to arbitrator {} with pub key ring {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getPubKeyRing()); - processModel.getP2PService().sendEncryptedDirectMessage( - trade.getArbitrator().getNodeAddress(), - trade.getArbitrator().getPubKeyRing(), - request, - new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at arbitrator: offerId={}", PaymentAccountKeyRequest.class.getSimpleName(), trade.getId()); - complete(); - } - @Override - public void onFault(String errorMessage) { - log.warn("Failed to send {} to arbitrator, error={}.", PaymentAccountKeyRequest.class.getSimpleName(), errorMessage); - failed(); - } - }); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index a8c3ec1664..e15b6a1b08 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -63,7 +63,7 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { trade.getCounterCurrencyExtraData(), deterministicId, trade.getPayoutTxHex(), - trade.getBuyer().getUpdatedMultisigHex(), + trade.getSelf().getUpdatedMultisigHex(), trade.getSelf().getPaymentAccountKey() ); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPayoutTxPublishedMessage.java deleted file mode 100644 index bef3626b90..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerSendPayoutTxPublishedMessage.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - -import static com.google.common.base.Preconditions.checkNotNull; - -import bisq.common.crypto.PubKeyRing; -import bisq.common.taskrunner.TaskRunner; -import bisq.core.payment.PaymentAccount; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayoutTxPublishedMessage; -import bisq.core.trade.messages.TradeMailboxMessage; -import bisq.network.p2p.NodeAddress; -import java.util.UUID; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class BuyerSendPayoutTxPublishedMessage extends SendMailboxMessageTask { - - public BuyerSendPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected NodeAddress getReceiverNodeAddress() { - return trade.getArbitrator().getNodeAddress(); - } - - @Override - protected PubKeyRing getReceiverPubKeyRing() { - return trade.getArbitrator().getPubKeyRing(); - } - - @Override - protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { - checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null"); - return new PayoutTxPublishedMessage( - tradeId, - processModel.getMyNodeAddress(), - trade.isMaker(), - null, // TODO: send witness data? - trade.getPayoutTxHex() - ); - } - - @Override - protected void setStateSent() { - log.info("Buyer sent PayoutTxPublishedMessage: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } - - @Override - protected void setStateArrived() { - log.info("Buyer's PayoutTxPublishedMessage arrived: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } - - @Override - protected void setStateStoredInMailbox() { - log.info("Buyer's PayoutTxPublishedMessage stored in mailbox: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } - - @Override - protected void setStateFault() { - log.error("Buyer's PayoutTxPublishedMessage failed: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index a51e41495c..076687dfdd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -74,7 +74,7 @@ public class MaybeSendSignContractRequest extends TradeTask { // create deposit tx and freeze inputs MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade); - + // TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig // save process state diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java new file mode 100644 index 0000000000..bd64221d48 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -0,0 +1,68 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + + +import bisq.common.taskrunner.TaskRunner; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositsConfirmedMessage; +import bisq.core.trade.protocol.TradingPeer; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProcessDepositsConfirmedMessage extends TradeTask { + + @SuppressWarnings({"unused"}) + public ProcessDepositsConfirmedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // get sender based on the pub key + // TODO: trade.getTradingPeer(PubKeyRing) + DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); + TradingPeer sender; + if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator(); + else if (trade.getBuyer().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getBuyer(); + else if (trade.getSeller().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getSeller(); + else throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); + + // update peer node address + sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + // decrypt seller payment account payload if key given + if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) { + log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey()); + trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); + } + + // store updated multisig hex for processing on payment sent + sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); + + // persist and complete + processModel.getTradeManager().requestPersistence(); + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index 00a19f3bd7..b30c0c5667 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -122,7 +122,7 @@ public class ProcessInitMultisigRequest extends TradeTask { log.info("Importing exchanged multisig hex for trade {}", trade.getId()); MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getExchangedMultisigHex(), peers[1].getExchangedMultisigHex()), xmrWalletService.getWalletPassword()); processModel.setMultisigAddress(result.getAddress()); - processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); // save and close multisig wallet once it's created + processModel.getProvider().getXmrWalletService().saveWallet(multisigWallet); // save multisig wallet once it's created trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java index 134def2644..2672ea0bc8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitTradeRequest.java @@ -22,7 +22,7 @@ import bisq.core.offer.Offer; import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.protocol.TradingPeer; @@ -71,7 +71,7 @@ public class ProcessInitTradeRequest extends TradeTask { if (!trade.getTaker().getNodeAddress().equals(request.getTakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); if (trade.getTaker().getPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest"); trade.getTaker().setPubKeyRing(request.getPubKeyRing()); - if (!TradeUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature + if (!HavenoUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature // check trade price try { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerProcessPaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java similarity index 50% rename from core/src/main/java/bisq/core/trade/protocol/tasks/BuyerProcessPaymentReceivedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index ff64d71717..94b7f2cab4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerProcessPaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -19,26 +19,22 @@ package bisq.core.trade.protocol.tasks; import bisq.core.account.sign.SignedWitness; import bisq.core.btc.wallet.XmrWalletService; +import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.util.Validator; - +import common.utils.GenUtils; import bisq.common.taskrunner.TaskRunner; -import java.util.List; - import lombok.extern.slf4j.Slf4j; +import monero.wallet.MoneroWallet; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; - - -import monero.wallet.MoneroWallet; - @Slf4j -public class BuyerProcessPaymentReceivedMessage extends TradeTask { - public BuyerProcessPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { +public class ProcessPaymentReceivedMessage extends TradeTask { + public ProcessPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -50,39 +46,44 @@ public class BuyerProcessPaymentReceivedMessage extends TradeTask { PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage(); Validator.checkTradeId(processModel.getOfferId(), message); checkNotNull(message); - checkArgument(message.getPayoutTxHex() != null); + checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided"); // update to the latest peer address of our peer if the message is correct - trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests sometimes reuse addresses - // handle if payout tx is not seen on network - if (trade.getPhase().ordinal() < Trade.Phase.PAYOUT_PUBLISHED.ordinal()) { + // handle if payout tx not published + if (!trade.isPayoutPublished()) { - // publish payout tx if signed. otherwise verify, sign, and publish payout tx - boolean previouslySigned = trade.getPayoutTxHex() != null; - if (previouslySigned) { - log.info("Buyer publishing signed payout tx from seller"); - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); - List txHashes = multisigWallet.submitMultisigTxHex(message.getPayoutTxHex()); - trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0))); - XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx()); - trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG); - walletService.closeMultisigWallet(trade.getId()); - } else { - log.info("Buyer verifying, signing, and publishing seller's payout tx"); - trade.verifyPayoutTx(message.getPayoutTxHex(), true, true); - trade.setStateIfValidTransitionTo(Trade.State.BUYER_PUBLISHED_PAYOUT_TX); - // TODO (woodser): send PayoutTxPublishedMessage to seller + // import multisig hex + MoneroWallet multisigWallet = trade.getWallet(); + if (message.getUpdatedMultisigHex() != null) { + multisigWallet.importMultisigHex(message.getUpdatedMultisigHex()); + trade.saveWallet(); } - // mark address entries as available - processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId()); + // arbitrator waits for buyer to sign and broadcast payout tx if message arrived + boolean isSigned = message.getSignedPayoutTxHex() != null; + if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) { + log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName()); + GenUtils.waitFor(30000); + multisigWallet.rescanSpent(); + } + + // verify and publish payout tx + if (!trade.isPayoutPublished()) { + if (isSigned) { + log.info("{} publishing signed payout tx from seller", trade.getClass().getSimpleName()); + trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true); + } else { + log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName()); + trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true); + } + } } else { - log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); + log.info("We got the payout tx already set from the payout listener and do nothing here. trade ID={}", trade.getId()); } - // TODO: remove witness SignedWitness signedWitness = message.getSignedWitness(); if (signedWitness != null) { // We received the signedWitness from the seller and publish the data to the network. @@ -91,6 +92,8 @@ public class BuyerProcessPaymentReceivedMessage extends TradeTask { processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness); } + // complete + if (!trade.isArbitrator()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator trade completes on payout published processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java index cfbb46dc29..f3f9fd768a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractResponse.java @@ -72,9 +72,6 @@ public class ProcessSignContractResponse extends TradeTask { // send deposit request when all contract signatures received if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { - // start listening for deposit txs - trade.listenForDepositTxs(); - // create request for arbitrator to deposit funds to multisig DepositRequest request = new DepositRequest( trade.getOffer().getId(), diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java deleted file mode 100644 index cb427c11b7..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - -import bisq.core.trade.Trade; -import bisq.core.trade.messages.UpdateMultisigRequest; -import bisq.core.trade.messages.UpdateMultisigResponse; - -import bisq.network.p2p.SendDirectMessageListener; - -import bisq.common.app.Version; -import bisq.common.taskrunner.TaskRunner; - -import java.util.Arrays; -import java.util.Date; -import java.util.UUID; - -import lombok.extern.slf4j.Slf4j; - -import static bisq.core.util.Validator.checkTradeId; -import static com.google.common.base.Preconditions.checkNotNull; - - - -import monero.wallet.MoneroWallet; - -@Slf4j -public class ProcessUpdateMultisigRequest extends TradeTask { - - @SuppressWarnings({"unused"}) - public ProcessUpdateMultisigRequest(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - log.debug("current trade state " + trade.getState()); - UpdateMultisigRequest request = (UpdateMultisigRequest) processModel.getTradeMessage(); - checkNotNull(request); - checkTradeId(processModel.getOfferId(), request); - MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId()); - - System.out.println("PROCESS UPDATE MULTISIG REQUEST"); - System.out.println(request); - - // check if multisig wallet needs updated - if (!multisigWallet.isMultisigImportNeeded()) { - log.warn("Multisig wallet does not need updated, so request is unexpected"); - failed(); // TODO (woodser): ignore instead fail - return; - } - - // get updated multisig hex - multisigWallet.sync(); - String updatedMultisigHex = multisigWallet.exportMultisigHex(); - - // import the multisig hex - int numOutputsSigned = multisigWallet.importMultisigHex(request.getUpdatedMultisigHex()); - System.out.println("Num outputs signed by imported multisig hex: " + numOutputsSigned); - - // close multisig wallet - processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); - - // respond with updated multisig hex - UpdateMultisigResponse response = new UpdateMultisigResponse( - processModel.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - new Date().getTime(), - updatedMultisigHex); - - log.info("Send {} with offerId {} and uid {} to peer {}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid(), trade.getTradingPeer().getNodeAddress()); - processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), response, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at trading peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid()); - complete(); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), response.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage); - appendToErrorMessage("Sending response failed: response=" + response + "\nerrorMessage=" + errorMessage); - failed(); - } - }); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerMaybeSendPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerMaybeSendPayoutTxPublishedMessage.java deleted file mode 100644 index 215144df70..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerMaybeSendPayoutTxPublishedMessage.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - -import static com.google.common.base.Preconditions.checkNotNull; - -import bisq.common.crypto.PubKeyRing; -import bisq.common.taskrunner.TaskRunner; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayoutTxPublishedMessage; -import bisq.core.trade.messages.TradeMailboxMessage; -import bisq.network.p2p.NodeAddress; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class SellerMaybeSendPayoutTxPublishedMessage extends SendMailboxMessageTask { - - public SellerMaybeSendPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // skip if payout tx not published - if (trade.getPhase().ordinal() < Trade.Phase.PAYOUT_PUBLISHED.ordinal()) { - complete(); - return; - } - - super.run(); - } catch (Throwable t) { - failed(t); - } - } - - @Override - protected NodeAddress getReceiverNodeAddress() { - return trade.getArbitrator().getNodeAddress(); - } - - @Override - protected PubKeyRing getReceiverPubKeyRing() { - return trade.getArbitrator().getPubKeyRing(); - } - - @Override - protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { - checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null"); - return new PayoutTxPublishedMessage( - tradeId, - processModel.getMyNodeAddress(), - trade.isMaker(), - null, // TODO: send witness data? - trade.getPayoutTxHex() - ); - } - - @Override - protected void setStateSent() { - log.info("Seller sent PayoutTxPublishedMessage: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } - - @Override - protected void setStateArrived() { - log.info("Seller's PayoutTxPublishedMessage arrived: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } - - @Override - protected void setStateStoredInMailbox() { - log.info("Seller's PayoutTxPublishedMessage stored in mailbox: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } - - @Override - protected void setStateFault() { - log.error("Seller's PayoutTxPublishedMessage failed: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress()); - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index 8348d6b9ca..9a8907db42 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -17,11 +17,16 @@ package bisq.core.trade.protocol.tasks; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.trade.Trade; + +import java.util.ArrayList; +import java.util.List; + import bisq.common.taskrunner.TaskRunner; import lombok.extern.slf4j.Slf4j; - +import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxWallet; @Slf4j @@ -37,25 +42,35 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { try { runInterceptHook(); + // import multisig hex + MoneroWallet multisigWallet = trade.getWallet(); + List updatedMultisigHexes = new ArrayList(); + if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex()); + if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex()); + if (!updatedMultisigHexes.isEmpty()) { + multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); + trade.saveWallet(); + } + // verify, sign, and publish payout tx if given. otherwise create payout tx if (trade.getPayoutTxHex() != null) { - log.info("Seller verifying, signing, and publishing payout tx"); + log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId()); trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true); - - // mark address entries as available - processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId()); } else { // create unsigned payout tx - log.info("Seller creating unsigned payout tx"); + log.info("Seller creating unsigned payout tx for trade {}", trade.getId()); MoneroTxWallet payoutTx = trade.createPayoutTx(); - System.out.println("created payout tx: " + payoutTx); trade.setPayoutTx(payoutTx); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); - // start listening for published payout tx - trade.listenForPayoutTx(); + // export multisig hex once + if (trade.getSelf().getUpdatedMultisigHex() == null) { + trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); + } } + + processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java index 2db1e918b6..6526f64a72 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerProcessPaymentSentMessage.java @@ -20,12 +20,10 @@ package bisq.core.trade.protocol.tasks; import static com.google.common.base.Preconditions.checkNotNull; import bisq.common.taskrunner.TaskRunner; -import bisq.core.btc.wallet.XmrWalletService; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.util.Validator; import lombok.extern.slf4j.Slf4j; -import monero.wallet.MoneroWallet; @Slf4j public class SellerProcessPaymentSentMessage extends TradeTask { @@ -47,18 +45,10 @@ public class SellerProcessPaymentSentMessage extends TradeTask { trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); // decrypt buyer's payment account payload - trade.decryptPeersPaymentAccountPayload(message.getPaymentAccountKey()); - - // sync and update multisig wallet - if (trade.getBuyer().getUpdatedMultisigHex() != null) { - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // TODO: ensure sync() always called before importMultisigHex() - multisigWallet.importMultisigHex(trade.getBuyer().getUpdatedMultisigHex()); - walletService.closeMultisigWallet(trade.getId()); - } + trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); // update latest peer address - trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); + trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) { @@ -73,7 +63,6 @@ public class SellerProcessPaymentSentMessage extends TradeTask { trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); - complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index 08f5d86dec..85b4d6f2b2 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -21,6 +21,8 @@ import bisq.core.account.sign.SignedWitness; import bisq.core.trade.Trade; import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.TradeMailboxMessage; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; import bisq.common.taskrunner.TaskRunner; import lombok.EqualsAndHashCode; @@ -30,13 +32,17 @@ import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode(callSuper = true) @Slf4j -public class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { +public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { SignedWitness signedWitness = null; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } + protected abstract NodeAddress getReceiverNodeAddress(); + + protected abstract PubKeyRing getReceiverPubKeyRing(); + @Override protected void run() { try { @@ -55,47 +61,52 @@ public class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { } @Override - protected TradeMailboxMessage getTradeMailboxMessage(String id) { + protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null"); + + // TODO: sign witness + // AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); + // if (accountAgeWitnessService.isSignWitnessTrade(trade)) { + // // Broadcast is done in accountAgeWitness domain. + // accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); + // } + return new PaymentReceivedMessage( - id, + tradeId, processModel.getMyNodeAddress(), signedWitness, - trade.getPayoutTxHex() + trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned + trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed + trade.getSelf().getUpdatedMultisigHex(), + trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout ); } - // TODO: using PAYOUT_TX_PUBLISHED_MSG to represent PAYMENT_RECEIVED_MSG after payout, but PAYOUT_TX_PUBLISHED_MSG is specifically for arbitrator. delete *PAYOUT_TX_PUBLISHED* messages and check payout field manually? - @Override protected void setStateSent() { - trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); - log.info("Sent SellerReceivedPaymentMessage: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness); - processModel.getTradeManager().requestPersistence(); - } - - @Override - protected void setStateArrived() { - trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); - log.info("Seller's PaymentReceivedMessage arrived: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness); - processModel.getTradeManager().requestPersistence(); - } - - @Override - protected void setStateStoredInMailbox() { - trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); - log.info("Seller's PaymentReceivedMessage stored in mailbox: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness); + trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); + log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); - log.error("SellerReceivedPaymentMessage failed: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness); + trade.setStateIfProgress(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setStateIfProgress(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); + log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateArrived() { + trade.setStateIfProgress(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); + log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); processModel.getTradeManager().requestPersistence(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToArbitrator.java similarity index 61% rename from core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToArbitrator.java index e2defbec2b..e04dcaec0d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupDepositTxsListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToArbitrator.java @@ -17,26 +17,27 @@ package bisq.core.trade.protocol.tasks; -import bisq.common.taskrunner.TaskRunner; import bisq.core.trade.Trade; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; +@EqualsAndHashCode(callSuper = true) @Slf4j -public class SetupDepositTxsListener extends TradeTask { +public class SellerSendPaymentReceivedMessageToArbitrator extends SellerSendPaymentReceivedMessage { - @SuppressWarnings({ "unused" }) - public SetupDepositTxsListener(TaskRunner taskHandler, Trade trade) { + public SellerSendPaymentReceivedMessageToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } - @Override - protected void run() { - try { - runInterceptHook(); - trade.listenForDepositTxs(); - complete(); - } catch (Throwable t) { - failed(t); - } + protected NodeAddress getReceiverNodeAddress() { + return trade.getArbitrator().getNodeAddress(); + } + + protected PubKeyRing getReceiverPubKeyRing() { + return trade.getArbitrator().getPubKeyRing(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java new file mode 100644 index 0000000000..c4571c6777 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java @@ -0,0 +1,52 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentReceivedMessage { + + public SellerSendPaymentReceivedMessageToBuyer(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected NodeAddress getReceiverNodeAddress() { + return trade.getBuyer().getNodeAddress(); + } + + protected PubKeyRing getReceiverPubKeyRing() { + return trade.getBuyer().getPubKeyRing(); + } + + // continue execution on fault so payment received message is sent to arbitrator + @Override + protected void onFault(String errorMessage, TradeMessage message) { + setStateFault(); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + complete(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentAccountPayloadKey.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java similarity index 75% rename from core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentAccountPayloadKey.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index ebc74f0f60..15235760e4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SellerSendPaymentAccountPayloadKey.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -18,23 +18,25 @@ package bisq.core.trade.protocol.tasks; import bisq.core.btc.wallet.XmrWalletService; +import bisq.core.trade.BuyerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PaymentAccountKeyResponse; +import bisq.core.trade.messages.DepositsConfirmedMessage; import bisq.core.trade.messages.TradeMailboxMessage; -import bisq.common.app.Version; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; import bisq.common.taskrunner.TaskRunner; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; /** - * Allow sender's payment account info to be decrypted when trade state is confirmed. + * Send message on first confirmation to decrypt peer payment account and update multisig hex. */ @Slf4j -public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask { - private PaymentAccountKeyResponse message; +public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask { + private DepositsConfirmedMessage message; - public SellerSendPaymentAccountPayloadKey(TaskRunner taskHandler, Trade trade) { + public SendDepositsConfirmedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -47,16 +49,22 @@ public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask { failed(t); } } - + + @Override + protected abstract NodeAddress getReceiverNodeAddress(); + + @Override + protected abstract PubKeyRing getReceiverPubKeyRing(); + @Override protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { if (message == null) { - // get updated multisig hex + // export multisig hex once if (trade.getSelf().getUpdatedMultisigHex() == null) { XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId); - trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); // only export multisig hex once + trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); } // 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 @@ -64,13 +72,12 @@ public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask { // messages where only the one which gets processed by the peer would be removed we use the same uid. All // other data stays the same when we re-send the message at any time later. String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress(); - message = new PaymentAccountKeyResponse( + message = new DepositsConfirmedMessage( trade.getOffer().getId(), processModel.getMyNodeAddress(), processModel.getPubKeyRing(), deterministicId, - Version.getP2PMessageVersion(), - trade.getSelf().getPaymentAccountKey(), + getReceiverNodeAddress().equals(trade.getBuyer().getNodeAddress()) ? trade.getSeller().getPaymentAccountKey() : null, // buyer receives seller's payment account decryption key trade.getSelf().getUpdatedMultisigHex()); } return message; diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java new file mode 100644 index 0000000000..4f6cd4aa99 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java @@ -0,0 +1,46 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +/** + * Send message on first confirmation to decrypt peer payment account and update multisig hex. + */ +@Slf4j +public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfirmedMessage { + + public SendDepositsConfirmedMessageToArbitrator(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + public NodeAddress getReceiverNodeAddress() { + return trade.getArbitrator().getNodeAddress(); + } + + @Override + public PubKeyRing getReceiverPubKeyRing() { + return trade.getArbitrator().getPubKeyRing(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java new file mode 100644 index 0000000000..4302d455b3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java @@ -0,0 +1,46 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +/** + * Send message on first confirmation to decrypt peer payment account and update multisig hex. + */ +@Slf4j +public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMessage { + + public SendDepositsConfirmedMessageToBuyer(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + public NodeAddress getReceiverNodeAddress() { + return trade.getBuyer().getNodeAddress(); + } + + @Override + public PubKeyRing getReceiverPubKeyRing() { + return trade.getBuyer().getPubKeyRing(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java new file mode 100644 index 0000000000..594de810ee --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java @@ -0,0 +1,46 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.network.p2p.NodeAddress; +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +/** + * Send message on first confirmation to decrypt peer payment account and update multisig hex. + */ +@Slf4j +public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedMessage { + + public SendDepositsConfirmedMessageToSeller(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + public NodeAddress getReceiverNodeAddress() { + return trade.getSeller().getNodeAddress(); + } + + @Override + public PubKeyRing getReceiverPubKeyRing() { + return trade.getSeller().getPubKeyRing(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java deleted file mode 100644 index 24056e560c..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - -import bisq.common.UserThread; -import bisq.common.taskrunner.TaskRunner; -import bisq.core.trade.Trade; -import lombok.extern.slf4j.Slf4j; -import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.Subscription; - -@Slf4j -public class SetupPayoutTxListener extends TradeTask { - - private Subscription tradeStateSubscription; - - @SuppressWarnings({ "unused" }) - public SetupPayoutTxListener(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // skip if payout already published - if (!trade.isPayoutPublished()) { - - // listen for payout tx - trade.listenForPayoutTx(); - tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { - if (trade.isPayoutPublished()) { - - // cleanup on trade completion - processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId()); - UserThread.execute(this::unSubscribe); // unsubscribe - } - }); - } - - complete(); - } catch (Throwable t) { - failed(t); - } - } - - private void unSubscribe() { - if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe(); - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java deleted file mode 100644 index 5d4983a1bc..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * This file is part of Haveno. - * - * Haveno is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Haveno is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Haveno. If not, see . - */ - -package bisq.core.trade.protocol.tasks; - -import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.TradeMessage; -import bisq.core.trade.messages.UpdateMultisigRequest; -import bisq.core.trade.messages.UpdateMultisigResponse; -import bisq.core.trade.protocol.TradeListener; - -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.SendDirectMessageListener; - -import bisq.common.app.Version; -import bisq.common.taskrunner.TaskRunner; - -import java.util.Date; -import java.util.UUID; - -import lombok.extern.slf4j.Slf4j; - - - -import monero.wallet.MoneroWallet; - -@Slf4j -public class UpdateMultisigWithTradingPeer extends TradeTask { - - private TradeListener updateMultisigResponseListener; - - @SuppressWarnings({"unused"}) - public UpdateMultisigWithTradingPeer(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // fetch relevant trade info - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerPreparesPaymentStartedMessage - - // skip if multisig wallet does not need updated - if (!multisigWallet.isMultisigImportNeeded()) { - log.warn("Multisig wallet does not need updated, this should not happen"); - failed(); - return; - } - - // register listener to receive updated multisig response - updateMultisigResponseListener = new TradeListener() { - @Override - public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) { - if (!(message instanceof UpdateMultisigResponse)) return; - UpdateMultisigResponse response = (UpdateMultisigResponse) message; - multisigWallet.sync(); - multisigWallet.importMultisigHex(response.getUpdatedMultisigHex()); - trade.removeListener(updateMultisigResponseListener); - complete(); - } - }; - trade.addListener(updateMultisigResponseListener); - - // get updated multisig hex - multisigWallet.sync(); - String updatedMultisigHex = multisigWallet.exportMultisigHex(); - - // message trading peer with updated multisig hex - UpdateMultisigRequest message = new UpdateMultisigRequest( - processModel.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - new Date().getTime(), - updatedMultisigHex); - - System.out.println("Sending message: " + message); - - // TODO (woodser): trade.getTradingPeer().getNodeAddress() and/or trade.getTradingPeer().getPubKeyRing() are null on restart of application, so cannot send payment to complete trade - log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid(), trade.getTradingPeer().getNodeAddress()); - processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), message, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at trading peer: offerId={}; uid={}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", message.getClass().getSimpleName(), message.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); - failed(); - } - }); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 93991c72ec..415ed0c28c 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -31,8 +31,8 @@ import bisq.proto.grpc.GetTradeReply; import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.GetTradesReply; import bisq.proto.grpc.GetTradesRequest; -import bisq.proto.grpc.KeepFundsReply; -import bisq.proto.grpc.KeepFundsRequest; +import bisq.proto.grpc.CompleteTradeReply; +import bisq.proto.grpc.CompleteTradeRequest; import bisq.proto.grpc.SendChatMessageReply; import bisq.proto.grpc.SendChatMessageRequest; import bisq.proto.grpc.TakeOfferReply; @@ -176,13 +176,13 @@ class GrpcTradesService extends TradesImplBase { } } - // TODO: rename KeepFundsRequest to CloseTradeRequest + // TODO: rename CompleteTradeRequest to CloseTradeRequest @Override - public void keepFunds(KeepFundsRequest req, - StreamObserver responseObserver) { + public void completeTrade(CompleteTradeRequest req, + StreamObserver responseObserver) { try { coreApi.closeTrade(req.getTradeId()); - var reply = KeepFundsReply.newBuilder().build(); + var reply = CompleteTradeReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); } catch (Throwable cause) { @@ -244,12 +244,12 @@ class GrpcTradesService extends TradesImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); - put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java index 8c64a738ff..21c3b2e39e 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcWalletsService.java @@ -415,7 +415,7 @@ class GrpcWalletsService extends WalletsImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(50, SECONDS)); + put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(100, SECONDS)); // TODO: why do tests make so many calls to get balances? put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getSendBtcMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); diff --git a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java index 718a72195d..2a4c3d616e 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -28,7 +28,7 @@ import bisq.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import bisq.core.offer.placeoffer.tasks.ValidateOffer; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; -import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage; +import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; import bisq.core.trade.protocol.tasks.MakerSetLockTime; import bisq.core.trade.protocol.tasks.RemoveOffer; @@ -36,8 +36,7 @@ import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage; import bisq.core.trade.protocol.tasks.SellerPublishDepositTx; import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics; -import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage; -import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; +import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; import bisq.core.trade.protocol.tasks.TakerVerifyMakerFeePayment; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import bisq.common.taskrunner.Task; @@ -109,7 +108,7 @@ public class DebugView extends InitializableView { TakerVerifyMakerFeePayment.class, SellerPreparePaymentReceivedMessage.class, //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? - SellerSendPaymentReceivedMessage.class + SellerSendPaymentReceivedMessageToBuyer.class ) )); @@ -123,10 +122,9 @@ public class DebugView extends InitializableView { ApplyFilter.class, BuyerPreparePaymentSentMessage.class, - SetupPayoutTxListener.class, BuyerSendPaymentSentMessage.class, - BuyerProcessPaymentReceivedMessage.class + ProcessPaymentReceivedMessage.class ) )); @@ -142,10 +140,9 @@ public class DebugView extends InitializableView { ApplyFilter.class, TakerVerifyMakerFeePayment.class, BuyerPreparePaymentSentMessage.class, - SetupPayoutTxListener.class, BuyerSendPaymentSentMessage.class, - BuyerProcessPaymentReceivedMessage.class) + ProcessPaymentReceivedMessage.class) )); addGroup("SellerAsMakerProtocol", FXCollections.observableArrayList(Arrays.asList( @@ -166,7 +163,7 @@ public class DebugView extends InitializableView { ApplyFilter.class, SellerPreparePaymentReceivedMessage.class, //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? - SellerSendPaymentReceivedMessage.class + SellerSendPaymentReceivedMessageToBuyer.class ) )); } diff --git a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java index 9307c099f2..3556ad2996 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -42,7 +42,7 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferDirection; import bisq.core.offer.OpenOfferManager; import bisq.core.provider.price.PriceFeedService; -import bisq.core.trade.TradeUtils; +import bisq.core.trade.HavenoUtils; import bisq.core.user.Preferences; import bisq.core.util.VolumeUtil; diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 0c4d953e93..54131f4d8e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -424,8 +424,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im case PAYMENT_RECEIVED: appendMsg = Res.get("takeOffer.error.depositPublished"); break; - case PAYOUT_PUBLISHED: - case WITHDRAWN: + case COMPLETED: appendMsg = Res.get("takeOffer.error.payoutPublished"); break; default: @@ -444,7 +443,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } private void applyTradeState() { - if (trade.isTakerFeePublished()) { + if (trade.isDepositRequested()) { if (takeOfferResultHandler != null) takeOfferResultHandler.run(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java index 1d5a2e3795..f32d1766ca 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java @@ -183,12 +183,11 @@ public class NotificationCenter { private void onTradePhaseChanged(Trade trade, Trade.Phase phase) { String message = null; - if (trade.isPayoutPublished() && !trade.isWithdrawn()) { + if (trade.isPayoutPublished() && !trade.isCompleted()) { message = Res.get("notification.trade.completed"); } else { if (trade instanceof MakerTrade && - phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal() || - phase.ordinal() == Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) { + phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal()) { final String role = trade instanceof BuyerTrade ? Res.get("shared.seller") : Res.get("shared.buyer"); message = Res.get("notification.trade.accepted", role); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 43dc098b70..4f932cc098 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -590,7 +590,7 @@ public class DisputeSummaryWindow extends Overlay { Button cancelButton = tuple.second; closeTicketButton.setOnAction(e -> { - disputesService.resolveDisputePayout(dispute, disputeResult, contract); + disputesService.applyDisputePayout(dispute, disputeResult, contract); doClose(closeTicketButton); // if (dispute.getDepositTxSerialized() == null) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 4ded5304ec..77578fe345 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -200,7 +200,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { ((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentStarted(resultHandler, errorMessageHandler); } - public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Trade trade = getTrade(); checkNotNull(trade, "trade must not be null"); checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade"); @@ -466,7 +466,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { String payoutTxHashAsString = null; MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); String updatedMultisigHex = multisigWallet.exportMultisigHex(); - xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet if (trade.getPayoutTxId() != null) { // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxHashAsString = payoutTx.getHashAsString(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 56e620321c..2c60a0e12b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -30,8 +30,11 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferUtil; import bisq.core.provider.fee.FeeService; import bisq.core.provider.mempool.MempoolService; +import bisq.core.trade.ArbitratorTrade; +import bisq.core.trade.BuyerTrade; import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.Contract; +import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeUtil; import bisq.core.user.User; @@ -115,6 +118,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; + private Subscription payoutStateSubscription; private Subscription messageStateSubscription; @Getter protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty(); @@ -160,6 +164,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { UserThread.execute(() -> onTradeStateChanged(state)); }); + payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> { + UserThread.execute(() -> onPayoutStateChanged(state)); + }); messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentStartedMessageStateProperty(), this::onMessageStateChanged); } } @@ -399,6 +417,13 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { + model.dataModel.onPaymentReceived(() -> { }, errorMessage -> { busyAnimation.stop(); new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show(); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 00c528e465..8b7663c601 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -731,7 +731,7 @@ service Trades { } rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) { } - rpc KeepFunds (KeepFundsRequest) returns (KeepFundsReply) { + rpc CompleteTrade (CompleteTradeRequest) returns (CompleteTradeReply) { } rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) { } @@ -787,11 +787,11 @@ message GetTradesReply { repeated TradeInfo trades = 1; } -message KeepFundsRequest { +message CompleteTradeRequest { string trade_id = 1; } -message KeepFundsReply { +message CompleteTradeReply { } message WithdrawFundsRequest { @@ -837,15 +837,16 @@ message TradeInfo { string state = 16; string phase = 17; string period_state = 18; - bool is_deposit_published = 19; - bool is_deposit_unlocked = 20; - bool is_payment_sent = 21; - bool is_payment_received = 22; - bool is_payout_published = 23; - bool is_completed = 24; - string contract_as_json = 25; - ContractInfo contract = 26; - string trade_volume = 27; + string payout_state = 19; + bool is_deposit_published = 20; + bool is_deposit_unlocked = 21; + bool is_payment_sent = 22; + bool is_payment_received = 23; + bool is_payout_published = 24; + bool is_completed = 25; + string contract_as_json = 26; + ContractInfo contract = 27; + string trade_volume = 28; string maker_deposit_tx_id = 100; string taker_deposit_tx_id = 101; diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index dcd042faff..a0e0261a99 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -74,17 +74,11 @@ message NetworkEnvelope { SignContractResponse sign_contract_response = 1006; DepositRequest deposit_request = 1007; DepositResponse deposit_response = 1008; - PaymentAccountKeyRequest payment_account_key_request = 1009; - PaymentAccountKeyResponse payment_account_key_response = 1010; - PaymentSentMessage payment_sent_message = 1011; - PaymentReceivedMessage payment_received_message = 1012; - PayoutTxPublishedMessage payout_tx_published_message = 1013; - ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1016; - ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1017; - - // TODO: delete these - UpdateMultisigRequest update_multisig_request = 1018; - UpdateMultisigResponse update_multisig_response = 1019; + DepositsConfirmedMessage deposits_confirmed_message = 1009; + PaymentSentMessage payment_sent_message = 1010; + PaymentReceivedMessage payment_received_message = 1011; + ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012; + ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013; } } @@ -355,37 +349,12 @@ message DepositResponse { int64 current_date = 5; } -message PaymentAccountKeyRequest { +message DepositsConfirmedMessage { string trade_id = 1; NodeAddress sender_node_address = 2; PubKeyRing pub_key_ring = 3; string uid = 4; -} - -message PaymentAccountKeyResponse { - string trade_id = 1; - NodeAddress sender_node_address = 2; - PubKeyRing pub_key_ring = 3; - string uid = 4; - bytes payment_account_key = 5; - string updated_multisig_hex = 6; -} - -message UpdateMultisigRequest { - string trade_id = 1; - NodeAddress sender_node_address = 2; - PubKeyRing pub_key_ring = 3; - string uid = 4; - int64 current_date = 5; - string updated_multisig_hex = 6; -} - -message UpdateMultisigResponse { - string trade_id = 1; - NodeAddress sender_node_address = 2; - PubKeyRing pub_key_ring = 3; - string uid = 4; - int64 current_date = 5; + bytes seller_payment_account_key = 5; string updated_multisig_hex = 6; } @@ -454,16 +423,10 @@ message PaymentReceivedMessage { NodeAddress sender_node_address = 2; string uid = 3; SignedWitness signed_witness = 4; // Added in v1.4.0 - string payout_tx_hex = 5; -} - -message PayoutTxPublishedMessage { - string trade_id = 1; - NodeAddress sender_node_address = 2; - bool is_maker = 3; - string uid = 4; - SignedWitness signed_witness = 5; + string unsigned_payout_tx_hex = 5; string signed_payout_tx_hex = 6; + string updated_multisig_hex = 7; + bool saw_arrived_payment_received_msg = 8; } message ArbitratorPayoutTxRequest { @@ -1644,33 +1607,24 @@ message Trade { CONTRACT_SIGNATURE_REQUESTED = 6; CONTRACT_SIGNED = 7; SENT_PUBLISH_DEPOSIT_TX_REQUEST = 8; - SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 9; - STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 10; - SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 11; - ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 12; - DEPOSIT_TXS_SEEN_IN_NETWORK = 13; - DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 14; - DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 15; - BUYER_CONFIRMED_IN_UI_PAYMENT_SENT = 16; - BUYER_SENT_PAYMENT_SENT_MSG = 17; - BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 18; - BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 19; - BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 20; - SELLER_RECEIVED_PAYMENT_SENT_MSG = 21; - SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 22; - SELLER_SENT_PAYMENT_RECEIVED_MSG = 23; - SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 24; - SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25; - SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 26; - SELLER_PUBLISHED_PAYOUT_TX = 27; - SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 28; - SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 29; - SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 30; - SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 31; - BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 32; - BUYER_PUBLISHED_PAYOUT_TX = 33; - PAYOUT_TX_SEEN_IN_NETWORK = 34; - WITHDRAW_COMPLETED = 35; + SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 9; + SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 10; + ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 11; + DEPOSIT_TXS_SEEN_IN_NETWORK = 12; + DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 13; + DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 14; + BUYER_CONFIRMED_IN_UI_PAYMENT_SENT = 15; + BUYER_SENT_PAYMENT_SENT_MSG = 16; + BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 17; + BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 18; + BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 19; + SELLER_RECEIVED_PAYMENT_SENT_MSG = 20; + SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 21; + SELLER_SENT_PAYMENT_RECEIVED_MSG = 22; + SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 23; + SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 24; + SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 25; + TRADE_COMPLETED = 26; } enum Phase { @@ -1682,8 +1636,14 @@ message Trade { DEPOSITS_UNLOCKED = 5; PAYMENT_SENT = 6; PAYMENT_RECEIVED = 7; - PAYOUT_PUBLISHED = 8; - WITHDRAWN = 9; + COMPLETED = 8; + } + + enum PayoutState { + UNPUBLISHED = 0; + PUBLISHED = 1; + CONFIRMED = 2; + UNLOCKED = 3; } enum DisputeState { @@ -1718,23 +1678,24 @@ message Trade { int64 take_offer_date = 9; int64 price = 10; State state = 11; - DisputeState dispute_state = 12; - TradePeriodState period_state = 13; - Contract contract = 14; - string contract_as_json = 15; - bytes contract_hash = 16; - NodeAddress arbitrator_node_address = 17; - NodeAddress mediator_node_address = 18; - string error_message = 19; - string counter_currency_tx_id = 20; - repeated ChatMessage chat_message = 21; - MediationResultState mediation_result_state = 22; - int64 lock_time = 23; - NodeAddress refund_agent_node_address = 24; - RefundResultState refund_result_state = 25; - string counter_currency_extra_data = 26; - string asset_tx_proof_result = 27; // name of AssetTxProofResult enum - string uid = 28; + PayoutState payout_state = 12; + DisputeState dispute_state = 13; + TradePeriodState period_state = 14; + Contract contract = 15; + string contract_as_json = 16; + bytes contract_hash = 17; + NodeAddress arbitrator_node_address = 18; + NodeAddress mediator_node_address = 19; + string error_message = 20; + string counter_currency_tx_id = 21; + repeated ChatMessage chat_message = 22; + MediationResultState mediation_result_state = 23; + int64 lock_time = 24; + NodeAddress refund_agent_node_address = 25; + RefundResultState refund_result_state = 26; + string counter_currency_extra_data = 27; + string asset_tx_proof_result = 28; // name of AssetTxProofResult enum + string uid = 29; } message BuyerAsMakerTrade {