diff --git a/Makefile b/Makefile index 3dc04c03..3c2a058e 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ haveno: ./gradlew build # build haveno without tests -no-tests: +skip-tests: ./gradlew build -x test # quick build desktop and daemon apps without tests 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 90d0e276..58ac2c8e 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -37,7 +37,7 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYMENT_SENT; import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.State.*; import static java.lang.String.format; @@ -170,8 +170,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { continue; } else { assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG) + .setPhase(PAYMENT_SENT) .setFiatSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade); @@ -190,8 +190,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest { var trade = bobClient.getTrade(tradeId); Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); + t.getState().equals(SELLER_RECEIVED_PAYMENT_INITIATED_MSG.name()) + && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name())); for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { if (!tradeStateAndPhaseCorrect.test(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 191f913a..15c285f7 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -38,7 +38,7 @@ import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.cli.TableFormat.formatBalancesTbls; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; -import static bisq.core.trade.Trade.Phase.FIAT_SENT; +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.WITHDRAWN; import static bisq.core.trade.Trade.State.*; @@ -173,8 +173,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { } else { // Note: offer.state == available assertEquals(AVAILABLE.name(), trade.getOffer().getState()); - EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG) - .setPhase(FIAT_SENT) + EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG) + .setPhase(PAYMENT_SENT) .setFiatSent(true); verifyExpectedProtocolStatus(trade); logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade); @@ -193,8 +193,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest { var trade = aliceClient.getTrade(tradeId); Predicate tradeStateAndPhaseCorrect = (t) -> - t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name()) - && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name())); + t.getState().equals(SELLER_RECEIVED_PAYMENT_INITIATED_MSG.name()) + && (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name())); for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { if (!tradeStateAndPhaseCorrect.test(trade)) { log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.", 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 71ec8a45..52f6b004 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -13,18 +13,26 @@ import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntryList; import bisq.core.btc.setup.MoneroWalletRpcManager; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.offer.Offer; +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.util.ParsingUtils; import com.google.common.util.concurrent.Service.State; import com.google.inject.name.Named; +import common.utils.JsonUtils; import java.io.File; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -36,8 +44,12 @@ import monero.common.MoneroRpcConnection; import monero.common.MoneroUtils; import monero.daemon.MoneroDaemon; import monero.daemon.model.MoneroNetworkType; +import monero.daemon.model.MoneroOutput; +import monero.daemon.model.MoneroSubmitTxResult; +import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletRpc; +import monero.wallet.model.MoneroCheckTx; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSubaddress; @@ -217,6 +229,140 @@ public class XmrWalletService { } } + + /** + * Create the reserve tx and freeze its inputs. The deposit amount is returned + * to the sender's payout address. Additional funds are reserved to allow + * fluctuations in the mining fee. + * + * @param tradeFee is the trade fee + * @param depositAmount the amount needed for the trade minus the trade fee + * @return a transaction to reserve a trade + */ + public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount) { + MoneroWallet wallet = getWallet(); + synchronized (wallet) { + + // get expected mining fee + MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .addDestination(TradeUtils.FEE_ADDRESS, tradeFee) + .addDestination(returnAddress, depositAmount)); + BigInteger miningFee = miningFeeTx.getFee(); + + // create reserve tx + MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .addDestination(TradeUtils.FEE_ADDRESS, 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 + for (MoneroOutput input : reserveTx.getInputs()) { + wallet.freezeOutput(input.getKeyImage().getHex()); + } + + return reserveTx; + } + } + + /** + * Create the multisig deposit tx and freeze its inputs. + * + * @return MoneroTxWallet the multisig deposit tx + */ + public MoneroTxWallet createDepositTx(Trade trade) { + BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee()); + Offer offer = trade.getProcessModel().getOffer(); + BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit()); + String multisigAddress = trade.getProcessModel().getMultisigAddress(); + MoneroWallet wallet = getWallet(); + synchronized (wallet) { + + // create deposit tx + MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .addDestination(TradeUtils.FEE_ADDRESS, tradeFee) + .addDestination(multisigAddress, depositAmount)); + + // freeze deposit inputs + for (MoneroOutput input : depositTx.getInputs()) { + wallet.freezeOutput(input.getKeyImage().getHex()); + } + + return depositTx; + } + } + + /** + * Verify a reserve or deposit transaction used during trading. + * Checks double spends, deposit amount and destination, trade fee, and mining fee. + * The transaction is submitted but not relayed to the pool then flushed. + * + * @param depositAddress is the expected destination address for the deposit amount + * @param depositAmount is the expected amount deposited to multisig + * @param tradeFee is the expected fee for trading + * @param txHash is the transaction hash + * @param txHex is the transaction hex + * @param txKey is the transaction key + * @param keyImages are expected key images of inputs, ignored if null + * @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase + */ + public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List keyImages, boolean miningFeePadding) { + boolean submittedToPool = false; + MoneroDaemon daemon = getDaemon(); + MoneroWallet wallet = getWallet(); + try { + + // get tx from daemon + MoneroTx tx = daemon.getTx(txHash); + + // if tx is not submitted, submit but do not relay + if (tx == null) { + MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? + if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); + submittedToPool = true; + tx = daemon.getTx(txHash); + } else if (tx.isRelayed()) { + throw new RuntimeException("Trade tx must not be relayed"); + } + + // verify reserved key images + if (keyImages != null) { + Set txKeyImages = new HashSet(); + for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); + if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images"); + } + + // verify the unlock height + if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); + + // verify trade fee + String feeAddress = TradeUtils.FEE_ADDRESS; + 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()); + + // verify mining fee + BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate + BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee + tx = daemon.getTx(txHash); + if (tx.getFee().compareTo(feeThreshold) < 0) { + throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee()); + } + + // verify deposit amount + check = wallet.checkTxKey(txHash, txKey, depositAddress); + if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); + BigInteger depositThreshold = depositAmount; + if (miningFeePadding) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee) + if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount()); + } finally { + + // flush tx from pool if we added it + if (submittedToPool) daemon.flushTxPool(txHash); + } + } + public void shutDown() { closeAllWallets(); } 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 9ffc893b..377c62e6 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java @@ -76,12 +76,12 @@ public class TradeEvents { if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.conf", shortId); break; - case FIAT_SENT: + case PAYMENT_SENT: // We only notify the seller if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getSellerPubKeyRing())) msg = Res.get("account.notifications.trade.message.msg.started", shortId); break; - case FIAT_RECEIVED: + case PAYMENT_RECEIVED: break; case PAYOUT_PUBLISHED: // We only notify the buyer diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 0c857dff..395e4e03 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -664,9 +664,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Offer offer = new Offer(request.getOfferPayload()); BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(offer.getDirection() == OfferPayload.Direction.BUY ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); - TradeUtils.processTradeTx( - xmrWalletService.getDaemon(), - xmrWalletService.getWallet(), + xmrWalletService.verifyTradeTx( request.getPayoutAddress(), depositAmount, tradeFee, diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java index 8549f37d..2a2ee8af 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/MakerReservesTradeFunds.java @@ -22,13 +22,11 @@ import bisq.common.taskrunner.TaskRunner; import bisq.core.btc.model.XmrAddressEntry; import bisq.core.offer.Offer; import bisq.core.offer.placeoffer.PlaceOfferModel; -import bisq.core.trade.TradeUtils; import bisq.core.util.ParsingUtils; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import monero.daemon.model.MoneroOutput; -import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxWallet; public class MakerReservesTradeFunds extends Task { @@ -45,26 +43,22 @@ public class MakerReservesTradeFunds extends Task { try { runInterceptHook(); - // synchronize on wallet to reserve key images - synchronized (model.getXmrWalletService().getWallet()) { + // freeze trade funds and get reserve tx + String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); + BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer()); + MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount); - // create transaction to reserve trade - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); - BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer()); - MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount); + // collect reserved key images // TODO (woodser): switch to proof of reserve? + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - // collect reserved key images // TODO (woodser): switch to proof of reserve? - List reservedKeyImages = new ArrayList(); - for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - - // save offer state - // TODO (woodser): persist - model.setReserveTx(reserveTx); - offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); - offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field - complete(); - } + // save offer state + // TODO (woodser): persist + model.setReserveTx(reserveTx); + offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); + offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field + complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" 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 22b695c2..01a92e59 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -38,7 +38,8 @@ import bisq.core.support.dispute.messages.OpenNewDisputeMessage; 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.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.PaymentSentMessage; +import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; import bisq.core.trade.messages.DepositRequest; @@ -52,7 +53,7 @@ import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; import bisq.core.trade.messages.PaymentAccountPayloadRequest; -import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.messages.RefreshTradeStateRequest; import bisq.core.trade.messages.SignContractRequest; @@ -182,11 +183,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo case DEPOSIT_TX_AND_DELAYED_PAYOUT_TX_MESSAGE: return DepositTxAndDelayedPayoutTxMessage.fromProto(proto.getDepositTxAndDelayedPayoutTxMessage(), messageVersion); - case COUNTER_CURRENCY_TRANSFER_STARTED_MESSAGE: - return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), 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 PEER_PUBLISHED_DELAYED_PAYOUT_TX_MESSAGE: return PeerPublishedDelayedPayoutTxMessage.fromProto(proto.getPeerPublishedDelayedPayoutTxMessage(), messageVersion); case TRADER_SIGNED_WITNESS_MESSAGE: 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 fed8feec..019cc898 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -326,7 +326,7 @@ public abstract class DisputeManager> extends Sup // update arbitrator's multisig wallet MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); - multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex())); + multisigWallet.importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex()); log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId()); // close multisig wallet 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 bce35d6a..5db9bb27 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 @@ -380,7 +380,7 @@ public final class ArbitrationManager extends DisputeManager= Phase.FIAT_SENT.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal(); } public boolean isFiatReceived() { - return getState().getPhase().ordinal() >= Phase.FIAT_RECEIVED.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); } public boolean isPayoutPublished() { diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/TradeUtils.java index fa774cdf..528d0e9a 100644 --- a/core/src/main/java/bisq/core/trade/TradeUtils.java +++ b/core/src/main/java/bisq/core/trade/TradeUtils.java @@ -17,33 +17,16 @@ package bisq.core.trade; -import static com.google.common.base.Preconditions.checkNotNull; - import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.Sig; import bisq.common.util.Tuple2; import bisq.common.util.Utilities; -import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferPayload.Direction; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.trade.messages.InitTradeRequest; -import common.utils.JsonUtils; -import java.math.BigInteger; -import java.util.HashSet; -import java.util.List; import java.util.Objects; -import java.util.Set; -import monero.daemon.MoneroDaemon; -import monero.daemon.model.MoneroOutput; -import monero.daemon.model.MoneroSubmitTxResult; -import monero.daemon.model.MoneroTx; -import monero.wallet.MoneroWallet; -import monero.wallet.model.MoneroCheckTx; -import monero.wallet.model.MoneroTxConfig; -import monero.wallet.model.MoneroTxWallet; /** * Collection of utilities for trading. @@ -136,162 +119,6 @@ public class TradeUtils { } } - /** - * Create a transaction to reserve a trade and freeze its funds. The deposit - * amount is returned to the sender's payout address. Additional funds are - * reserved to allow fluctuations in the mining fee. - * - * @param xmrWalletService - * @param offerId - * @param tradeFee - * @param depositAmount - * @return a transaction to reserve a trade - */ - public static MoneroTxWallet reserveTradeFunds(XmrWalletService xmrWalletService, String offerId, BigInteger tradeFee, String returnAddress, BigInteger depositAmount) { - - // get expected mining fee - MoneroWallet wallet = xmrWalletService.getWallet(); - MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig() - .setAccountIndex(0) - .addDestination(TradeUtils.FEE_ADDRESS, tradeFee) - .addDestination(returnAddress, depositAmount)); - BigInteger miningFee = miningFeeTx.getFee(); - - // create reserve tx - MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig() - .setAccountIndex(0) - .addDestination(TradeUtils.FEE_ADDRESS, 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 trade funds - for (MoneroOutput input : reserveTx.getInputs()) { - wallet.freezeOutput(input.getKeyImage().getHex()); - } - - return reserveTx; - } - - /** - * Create a transaction to deposit funds to the multisig wallet. - * - * @param xmrWalletService - * @param tradeFee - * @param destinationAddress - * @param depositAddress - * @return MoneroTxWallet - */ - public static MoneroTxWallet createDepositTx(XmrWalletService xmrWalletService, BigInteger tradeFee, String depositAddress, BigInteger depositAmount) { - return xmrWalletService.getWallet().createTx(new MoneroTxConfig() - .setAccountIndex(0) - .addDestination(TradeUtils.FEE_ADDRESS, tradeFee) - .addDestination(depositAddress, depositAmount)); - } - - /** - * Process a reserve or deposit transaction used during trading. - * Checks double spends, deposit amount and destination, trade fee, and mining fee. - * The transaction is submitted but not relayed to the pool then flushed. - * - * @param daemon is the Monero daemon to check for double spends - * @param wallet is the Monero wallet to verify the tx - * @param depositAddress is the expected destination address for the deposit amount - * @param depositAmount is the expected amount deposited to multisig - * @param tradeFee is the expected fee for trading - * @param txHash is the transaction hash - * @param txHex is the transaction hex - * @param txKey is the transaction key - * @param keyImages are expected key images of inputs, ignored if null - * @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase - */ - public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List keyImages, boolean miningFeePadding) { - boolean submittedToPool = false; - try { - - // get tx from daemon - MoneroTx tx = daemon.getTx(txHash); - - // if tx is not submitted, submit but do not relay - if (tx == null) { - MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? - if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); - submittedToPool = true; - tx = daemon.getTx(txHash); - } else if (tx.isRelayed()) { - throw new RuntimeException("Trade tx must not be relayed"); - } - - // verify reserved key images - if (keyImages != null) { - Set txKeyImages = new HashSet(); - for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); - if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images"); - } - - // verify the unlock height - if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); - - // verify trade fee - String feeAddress = TradeUtils.FEE_ADDRESS; - 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()); - - // verify mining fee - BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate - BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee - tx = daemon.getTx(txHash); - if (tx.getFee().compareTo(feeThreshold) < 0) { - throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee()); - } - - // verify deposit amount - check = wallet.checkTxKey(txHash, txKey, depositAddress); - if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); - BigInteger depositThreshold = depositAmount; - if (miningFeePadding) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee) - if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount()); - } finally { - - // flush tx from pool if we added it - if (submittedToPool) daemon.flushTxPool(txHash); - } - } - - /** - * Create a contract from a trade. - * - * TODO (woodser): refactor/reduce trade, process model, and trading peer models - * - * @param trade is the trade to create the contract from - * @return the contract - */ - public static Contract createContract(Trade trade) { - boolean isBuyerMakerAndSellerTaker = trade.getOffer().getDirection() == Direction.BUY; - Contract contract = new Contract( - trade.getOffer().getOfferPayload(), - checkNotNull(trade.getTradeAmount()).value, - trade.getTradePrice().getValue(), - isBuyerMakerAndSellerTaker ? trade.getMakerNodeAddress() : trade.getTakerNodeAddress(), // buyer node address // TODO (woodser): use maker and taker node address instead of buyer and seller node address for consistency - isBuyerMakerAndSellerTaker ? trade.getTakerNodeAddress() : trade.getMakerNodeAddress(), // seller node address - trade.getArbitratorNodeAddress(), - isBuyerMakerAndSellerTaker, - trade instanceof MakerTrade ? trade.getProcessModel().getAccountId() : trade.getMaker().getAccountId(), // maker account id - trade instanceof TakerTrade ? trade.getProcessModel().getAccountId() : trade.getTaker().getAccountId(), // taker account id - checkNotNull(trade instanceof MakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getPaymentMethodId() : trade.getOffer().getOfferPayload().getPaymentMethodId()), // maker payment method id - checkNotNull(trade instanceof TakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getPaymentMethodId() : trade.getTaker().getPaymentMethodId()), // taker payment method id - trade instanceof MakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getHash() : trade.getMaker().getPaymentAccountPayloadHash(), // maker payment account payload hash - trade instanceof TakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getHash() : trade.getTaker().getPaymentAccountPayloadHash(), // maker payment account payload hash - trade.getMakerPubKeyRing(), - trade.getTakerPubKeyRing(), - trade instanceof MakerTrade ? trade.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : trade.getMaker().getPayoutAddressString(), // maker payout address - trade instanceof TakerTrade ? trade.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : trade.getTaker().getPayoutAddressString(), // taker payout address - trade.getLockTime(), - trade.getMaker().getDepositTxHash(), - trade.getTaker().getDepositTxHash() - ); - return contract; - } - // TODO (woodser): remove the following utitilites? // Returns if both are AVAILABLE, otherwise null diff --git a/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java new file mode 100644 index 00000000..e392e303 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/PaymentReceivedMessage.java @@ -0,0 +1,110 @@ +/* + * 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 PaymentReceivedMessage extends TradeMailboxMessage { + private final NodeAddress senderNodeAddress; + private final String payoutTxHex; + + // Added in v1.4.0 + @Nullable + private final SignedWitness signedWitness; + + public PaymentReceivedMessage(String tradeId, + NodeAddress senderNodeAddress, + @Nullable SignedWitness signedWitness, + String signedPayoutTxHex) { + this(tradeId, + senderNodeAddress, + signedWitness, + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + signedPayoutTxHex); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaymentReceivedMessage(String tradeId, + NodeAddress senderNodeAddress, + @Nullable SignedWitness signedWitness, + String uid, + String messageVersion, + String signedPayoutTxHex) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.signedWitness = signedWitness; + this.payoutTxHex = signedPayoutTxHex; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.PaymentReceivedMessage.Builder builder = protobuf.PaymentReceivedMessage.newBuilder() + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid) + .setPayoutTxHex(payoutTxHex); + Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); + return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build(); + } + + public static NetworkEnvelope fromProto(protobuf.PaymentReceivedMessage proto, String messageVersion) { + // There is no method to check for a nullable non-primitive data type object but we know that all fields + // are empty/null, so we check for the signature to see if we got a valid signedWitness. + protobuf.SignedWitness protoSignedWitness = proto.getSignedWitness(); + SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ? + SignedWitness.fromProto(protoSignedWitness) : + null; + return new PaymentReceivedMessage(proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + signedWitness, + proto.getUid(), + messageVersion, + proto.getPayoutTxHex()); + } + + @Override + public String toString() { + return "SellerReceivedPaymentMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n signedWitness=" + signedWitness + + ",\n payoutTxHex=" + payoutTxHex + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/CounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java similarity index 70% rename from core/src/main/java/bisq/core/trade/messages/CounterCurrencyTransferStartedMessage.java rename to core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java index ffc674ef..0337913e 100644 --- a/core/src/main/java/bisq/core/trade/messages/CounterCurrencyTransferStartedMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/PaymentSentMessage.java @@ -31,33 +31,38 @@ import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) @Value -public final class CounterCurrencyTransferStartedMessage extends TradeMailboxMessage { +public final class PaymentSentMessage extends TradeMailboxMessage { private final String buyerPayoutAddress; private final NodeAddress senderNodeAddress; - private final String buyerPayoutTxSigned; @Nullable private final String counterCurrencyTxId; + @Nullable + private final String payoutTxHex; + @Nullable + private final String updatedMultisigHex; // Added after v1.3.7 // We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets. @Nullable private String counterCurrencyExtraData; - public CounterCurrencyTransferStartedMessage(String tradeId, + public PaymentSentMessage(String tradeId, String buyerPayoutAddress, NodeAddress senderNodeAddress, - String buyerPayoutTxSigned, @Nullable String counterCurrencyTxId, @Nullable String counterCurrencyExtraData, - String uid) { + String uid, + String signedPayoutTxHex, + String updatedMultisigHex) { this(tradeId, buyerPayoutAddress, senderNodeAddress, - buyerPayoutTxSigned, counterCurrencyTxId, counterCurrencyExtraData, uid, - Version.getP2PMessageVersion()); + Version.getP2PMessageVersion(), + signedPayoutTxHex, + updatedMultisigHex); } @@ -65,59 +70,64 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMailboxMes // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private CounterCurrencyTransferStartedMessage(String tradeId, + private PaymentSentMessage(String tradeId, String buyerPayoutAddress, NodeAddress senderNodeAddress, - String buyerPayoutTxSigned, @Nullable String counterCurrencyTxId, @Nullable String counterCurrencyExtraData, String uid, - String messageVersion) { + String messageVersion, + @Nullable String signedPayoutTxHex, + @Nullable String updatedMultisigHex) { super(messageVersion, tradeId, uid); this.buyerPayoutAddress = buyerPayoutAddress; this.senderNodeAddress = senderNodeAddress; - this.buyerPayoutTxSigned = buyerPayoutTxSigned; this.counterCurrencyTxId = counterCurrencyTxId; this.counterCurrencyExtraData = counterCurrencyExtraData; + this.payoutTxHex = signedPayoutTxHex; + this.updatedMultisigHex = updatedMultisigHex; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - final protobuf.CounterCurrencyTransferStartedMessage.Builder builder = protobuf.CounterCurrencyTransferStartedMessage.newBuilder(); + final protobuf.PaymentSentMessage.Builder builder = protobuf.PaymentSentMessage.newBuilder(); builder.setTradeId(tradeId) .setBuyerPayoutAddress(buyerPayoutAddress) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setBuyerPayoutTxSigned(buyerPayoutTxSigned) .setUid(uid); Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); + Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); + Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); - return getNetworkEnvelopeBuilder().setCounterCurrencyTransferStartedMessage(builder).build(); + return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build(); } - public static CounterCurrencyTransferStartedMessage fromProto(protobuf.CounterCurrencyTransferStartedMessage proto, + public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto, String messageVersion) { - return new CounterCurrencyTransferStartedMessage(proto.getTradeId(), + return new PaymentSentMessage(proto.getTradeId(), proto.getBuyerPayoutAddress(), NodeAddress.fromProto(proto.getSenderNodeAddress()), - proto.getBuyerPayoutTxSigned(), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()), ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()), proto.getUid(), - messageVersion); + messageVersion, + ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()), + ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); } @Override public String toString() { - return "CounterCurrencyTransferStartedMessage{" + + return "PaymentSentMessage{" + "\n buyerPayoutAddress='" + buyerPayoutAddress + '\'' + ",\n senderNodeAddress=" + senderNodeAddress + ",\n counterCurrencyTxId=" + counterCurrencyTxId + ",\n counterCurrencyExtraData=" + counterCurrencyExtraData + ",\n uid='" + uid + '\'' + - ",\n buyerPayoutTxSigned=" + buyerPayoutTxSigned + + ",\n payoutTxHex=" + payoutTxHex + + ",\n updatedMultisigHex=" + updatedMultisigHex + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java index 4acbd7a0..596e11a4 100644 --- a/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/PayoutTxPublishedMessage.java @@ -37,23 +37,23 @@ import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) @Value public final class PayoutTxPublishedMessage extends TradeMailboxMessage { - private final String signedMultisigTxHex; private final NodeAddress senderNodeAddress; + private final String payoutTxHex; // Added in v1.4.0 @Nullable private final SignedWitness signedWitness; public PayoutTxPublishedMessage(String tradeId, - String signedMultisigTxHex, NodeAddress senderNodeAddress, - @Nullable SignedWitness signedWitness) { + @Nullable SignedWitness signedWitness, + String payoutTxHex) { this(tradeId, - signedMultisigTxHex, senderNodeAddress, signedWitness, UUID.randomUUID().toString(), - Version.getP2PMessageVersion()); + Version.getP2PMessageVersion(), + payoutTxHex); } @@ -62,24 +62,24 @@ public final class PayoutTxPublishedMessage extends TradeMailboxMessage { /////////////////////////////////////////////////////////////////////////////////////////// private PayoutTxPublishedMessage(String tradeId, - String signedMultisigTxHex, NodeAddress senderNodeAddress, @Nullable SignedWitness signedWitness, String uid, - String messageVersion) { + String messageVersion, + String payoutTxHex) { super(messageVersion, tradeId, uid); - this.signedMultisigTxHex = signedMultisigTxHex; this.senderNodeAddress = senderNodeAddress; this.signedWitness = signedWitness; + this.payoutTxHex = payoutTxHex; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.PayoutTxPublishedMessage.Builder builder = protobuf.PayoutTxPublishedMessage.newBuilder() .setTradeId(tradeId) - .setSignedMultisigTxHex(signedMultisigTxHex) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid); + .setUid(uid) + .setPayoutTxHex(payoutTxHex); Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); return getNetworkEnvelopeBuilder().setPayoutTxPublishedMessage(builder).build(); } @@ -92,19 +92,19 @@ public final class PayoutTxPublishedMessage extends TradeMailboxMessage { SignedWitness.fromProto(protoSignedWitness) : null; return new PayoutTxPublishedMessage(proto.getTradeId(), - proto.getSignedMultisigTxHex(), NodeAddress.fromProto(proto.getSenderNodeAddress()), signedWitness, proto.getUid(), - messageVersion); + messageVersion, + proto.getPayoutTxHex()); } @Override public String toString() { return "PayoutTxPublishedMessage{" + - "\n signedMultisigTxHex=" + signedMultisigTxHex + - ",\n senderNodeAddress=" + senderNodeAddress + + "\n senderNodeAddress=" + senderNodeAddress + ",\n signedWitness=" + signedWitness + + ",\n payoutTxHex=" + payoutTxHex + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java b/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java index 4da74a94..aa049a78 100644 --- a/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java +++ b/core/src/main/java/bisq/core/trade/messages/RefreshTradeStateRequest.java @@ -24,7 +24,7 @@ import lombok.Value; /** * Not used anymore since v1.4.0 - * We do the re-sending of the payment sent message via the BuyerSendCounterCurrencyTransferStartedMessage task on the + * We do the re-sending of the payment sent message via the BuyerSendPaymentSentMessage task on the * buyer side, so seller do not need to do anything interactively. */ @Deprecated 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 ea82e14d..194e9c2d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -26,7 +26,7 @@ import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.PaymentAccountPayloadRequest; -import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.protocol.tasks.ProcessDepositResponse; @@ -279,7 +279,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol // We keep the handler here in as well to make it more transparent which messages we expect @Override - protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { + protected void handle(PaymentReceivedMessage message, NodeAddress peer) { super.handle(message, peer); } 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 e683db45..749f1eb6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -29,7 +29,7 @@ import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.messages.PaymentAccountPayloadRequest; -import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.TradeMessage; @@ -296,7 +296,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol // We keep the handler here in as well to make it more transparent which messages we expect @Override - protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { + protected void handle(PaymentReceivedMessage message, NodeAddress peer) { super.handle(message, peer); } 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 e0f286ef..25ff1e27 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerProtocol.java @@ -21,15 +21,15 @@ import bisq.core.trade.BuyerTrade; import bisq.core.trade.Trade; import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; -import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.SetupDepositTxsListener; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.trade.protocol.tasks.UpdateMultisigWithTradingPeer; -import bisq.core.trade.protocol.tasks.buyer.BuyerCreateAndSignPayoutTx; -import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage; -import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerPreparesPaymentSentMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessesPaymentReceivedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendsPaymentSentMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; import bisq.network.p2p.NodeAddress; @@ -63,16 +63,16 @@ public abstract class BuyerProtocol extends DisputeProtocol { .setup(tasks(SetupDepositTxsListener.class)) .executeTasks(); - given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED) + given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) .with(BuyerEvent.STARTUP)) .setup(tasks(BuyerSetupPayoutTxListener.class)) // TODO (woodser): mirror deposit listener setup? .executeTasks(); - given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED) - .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG, - Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG) + given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) + .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG, + Trade.State.BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG) .with(BuyerEvent.STARTUP)) - .setup(tasks(BuyerSendCounterCurrencyTransferStartedMessage.class)) + .setup(tasks(BuyerSendsPaymentSentMessage.class)) .executeTasks(); } @@ -82,8 +82,8 @@ public abstract class BuyerProtocol extends DisputeProtocol { if (message instanceof DepositTxAndDelayedPayoutTxMessage) { handle((DepositTxAndDelayedPayoutTxMessage) message, peer); - } else if (message instanceof PayoutTxPublishedMessage) { - handle((PayoutTxPublishedMessage) message, peer); + } else if (message instanceof PaymentReceivedMessage) { + handle((PaymentReceivedMessage) message, peer); } } @@ -131,25 +131,28 @@ public abstract class BuyerProtocol extends DisputeProtocol { synchronized (trade) { // TODO (woodser): UpdateMultisigWithTradingPeer sends UpdateMultisigRequest and waits for UpdateMultisigResponse which is new thread, so synchronized (trade) in subsequent pipeline blocks forever if we hold on with countdown latch in this function System.out.println("BuyerProtocol.onPaymentStarted() has the lock!!!"); BuyerEvent event = BuyerEvent.PAYMENT_SENT; + CountDownLatch latch = new CountDownLatch(1); expect(phase(Trade.Phase.DEPOSIT_CONFIRMED) .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks(ApplyFilter.class, getVerifyPeersFeePaymentClass(), - UpdateMultisigWithTradingPeer.class, - BuyerCreateAndSignPayoutTx.class, - BuyerSetupPayoutTxListener.class, - BuyerSendCounterCurrencyTransferStartedMessage.class) + //UpdateMultisigWithTradingPeer.class, // TODO (woodser): can use this to test protocol with updated multisig from peer. peer should attempt to send updated multisig hex earlier as part of protocol. cannot use with countdown latch because response comes back in a separate thread and blocks on trade + BuyerPreparesPaymentSentMessage.class, + //BuyerSetupPayoutTxListener.class, + BuyerSendsPaymentSentMessage.class) .using(new TradeTaskRunner(trade, () -> { + latch.countDown(); resultHandler.handleResult(); handleTaskRunnerSuccess(event); }, (errorMessage) -> { + latch.countDown(); errorMessageHandler.handleErrorMessage(errorMessage); handleTaskRunnerFault(event, errorMessage); }))) - .run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED)) + .run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED)) .executeTasks(); } } @@ -158,18 +161,18 @@ public abstract class BuyerProtocol extends DisputeProtocol { // Incoming message Payout tx /////////////////////////////////////////////////////////////////////////////////////////// - protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) { - log.info("BuyerProtocol.handle(PayoutTxPublishedMessage)"); + protected void handle(PaymentReceivedMessage message, NodeAddress peer) { + log.info("BuyerProtocol.handle(SellerReceivedPaymentMessage)"); synchronized (trade) { processModel.setTradeMessage(message); processModel.setTempTradingPeerNodeAddress(peer); CountDownLatch latch = new CountDownLatch(1); - expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) + expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYOUT_PUBLISHED) .with(message) .from(peer)) .setup(tasks( getVerifyPeersFeePaymentClass(), - BuyerProcessPayoutTxPublishedMessage.class) + BuyerProcessesPaymentReceivedMessage.class) .using(new TradeTaskRunner(trade, () -> { latch.countDown(); @@ -200,8 +203,8 @@ public abstract class BuyerProtocol extends DisputeProtocol { handle((DelayedPayoutTxSignatureRequest) message, peer); } else if (message instanceof DepositTxAndDelayedPayoutTxMessage) { handle((DepositTxAndDelayedPayoutTxMessage) message, peer); - } else if (message instanceof PayoutTxPublishedMessage) { - handle((PayoutTxPublishedMessage) message, peer); + } else if (message instanceof PaymentReceivedMessage) { + handle((PaymentReceivedMessage) message, peer); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java index 79c97007..f37435d4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/DisputeProtocol.java @@ -62,8 +62,8 @@ public abstract class DisputeProtocol extends TradeProtocol { public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, - Trade.Phase.FIAT_SENT, - Trade.Phase.FIAT_RECEIVED) + Trade.Phase.PAYMENT_SENT, + Trade.Phase.PAYMENT_RECEIVED) .with(event) .preCondition(trade.getTradingPeer().getMediatedPayoutTxSignature() == null, () -> errorMessageHandler.handleErrorMessage("We have received already the signature from the peer.")) @@ -89,8 +89,8 @@ public abstract class DisputeProtocol extends TradeProtocol { public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED; expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, - Trade.Phase.FIAT_SENT, - Trade.Phase.FIAT_RECEIVED) + Trade.Phase.PAYMENT_SENT, + Trade.Phase.PAYMENT_RECEIVED) .with(event) .preCondition(trade.getPayoutTx() == null, () -> errorMessageHandler.handleErrorMessage("Payout tx is already published."))) @@ -118,8 +118,8 @@ public abstract class DisputeProtocol extends TradeProtocol { protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) { expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, - Trade.Phase.FIAT_SENT, - Trade.Phase.FIAT_RECEIVED) + Trade.Phase.PAYMENT_SENT, + Trade.Phase.PAYMENT_RECEIVED) .with(message) .from(peer)) .setup(tasks(ProcessMediatedPayoutSignatureMessage.class)) @@ -128,8 +128,8 @@ public abstract class DisputeProtocol extends TradeProtocol { protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) { expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, - Trade.Phase.FIAT_SENT, - Trade.Phase.FIAT_RECEIVED) + Trade.Phase.PAYMENT_SENT, + Trade.Phase.PAYMENT_RECEIVED) .with(message) .from(peer)) .setup(tasks(ProcessMediatedPayoutTxPublishedMessage.class)) @@ -168,8 +168,8 @@ public abstract class DisputeProtocol extends TradeProtocol { private void handle(PeerPublishedDelayedPayoutTxMessage message, NodeAddress peer) { expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, - Trade.Phase.FIAT_SENT, - Trade.Phase.FIAT_RECEIVED) + Trade.Phase.PAYMENT_SENT, + Trade.Phase.PAYMENT_RECEIVED) .with(message) .from(peer)) .setup(tasks(ProcessPeerPublishedDelayedPayoutTxMessage.class)) diff --git a/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java index 7b04e0f2..cc24fcbf 100644 --- a/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/FluentProtocol.java @@ -289,7 +289,7 @@ public class FluentProtocol { return Result.VALID.info(info); } else { String info = MessageFormat.format("We received a {0} but we are are not in the expected phase.\n" + - "This can be an expected case if we get a repeated CounterCurrencyTransferStartedMessage " + + "This can be an expected case if we get a repeated PaymentSentMessage " + "after we have already received one as the peer re-sends that message at each startup.\n" + "Expected phases={1},\nTrade phase={2},\nTrade state= {3},\ntradeId={4}", trigger, diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java index 5b6dae85..aabf9dfb 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -186,12 +186,10 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private boolean multisigSetupComplete; // TODO (woodser): redundant with multisigAddress existing, remove - @Nullable - transient private MoneroTxWallet buyerSignedPayoutTx; // TODO (woodser): remove // We want to indicate the user the state of the message delivery of the - // CounterCurrencyTransferStartedMessage. As well we do an automatic re-send in case it was not ACKed yet. + // PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet. // To enable that even after restart we persist the state. @Setter private ObjectProperty paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @@ -421,13 +419,4 @@ public class ProcessModel implements Model, PersistablePayload { public KeyRing getKeyRing() { return provider.getKeyRing(); } - - public void setBuyerSignedPayoutTx(MoneroTxWallet buyerSignedPayoutTx) { - this.buyerSignedPayoutTx = buyerSignedPayoutTx; - } - - @Nullable - public MoneroTxWallet getBuyerSignedPayoutTx() { - return buyerSignedPayoutTx; - } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index d2099c7b..5f8d7614 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -21,7 +21,7 @@ package bisq.core.trade.protocol; import bisq.core.trade.SellerAsMakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.Trade.State; -import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositTxMessage; import bisq.core.trade.messages.InitMultisigRequest; @@ -322,7 +322,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc // We keep the handler here in as well to make it more transparent which messages we expect @Override - protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { + protected void handle(PaymentSentMessage message, NodeAddress peer) { super.handle(message, peer); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index 3702630f..4c659b8c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -23,7 +23,7 @@ import bisq.core.trade.SellerAsTakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.Trade.State; import bisq.core.trade.handlers.TradeResultHandler; -import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InputsForDepositTxResponse; @@ -279,7 +279,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc // We keep the handler here in as well to make it more transparent which messages we expect @Override - protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { + protected void handle(PaymentSentMessage message, NodeAddress peer) { super.handle(message, peer); } 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 22482ebe..aee60f67 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerProtocol.java @@ -19,15 +19,15 @@ package bisq.core.trade.protocol; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.BuyerProtocol.BuyerEvent; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.SetupDepositTxsListener; import bisq.core.trade.protocol.tasks.TradeTask; -import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage; -import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; -import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerProcessesPaymentSentMessage; +import bisq.core.trade.protocol.tasks.seller.SellerSendsPaymentReceivedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerPreparesPaymentReceivedMessage; import bisq.network.p2p.NodeAddress; import java.util.concurrent.CountDownLatch; @@ -66,8 +66,8 @@ public abstract class SellerProtocol extends DisputeProtocol { public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { super.onMailboxMessage(message, peerNodeAddress); - if (message instanceof CounterCurrencyTransferStartedMessage) { - handle((CounterCurrencyTransferStartedMessage) message, peerNodeAddress); + if (message instanceof PaymentSentMessage) { + handle((PaymentSentMessage) message, peerNodeAddress); } } @@ -76,39 +76,31 @@ public abstract class SellerProtocol extends DisputeProtocol { // Incoming message when buyer has clicked payment started button /////////////////////////////////////////////////////////////////////////////////////////// - protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) { - log.info("SellerProtocol.handle(CounterCurrencyTransferStartedMessage)"); + protected void handle(PaymentSentMessage message, NodeAddress peer) { + log.info("SellerProtocol.handle(PaymentSentMessage)"); // We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case // that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received - // a mailbox message with CounterCurrencyTransferStartedMessage. + // a mailbox message with PaymentSentMessage. // TODO A better fix would be to add a listener for the wallet sync state and process // the mailbox msg once wallet is ready and trade state set. synchronized (trade) { - CountDownLatch latch = new CountDownLatch(1); + //CountDownLatch latch = new CountDownLatch(1); // TODO: apply latch countdown expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED) .with(message) .from(peer) .preCondition(trade.getPayoutTx() == null, () -> { - log.warn("We received a CounterCurrencyTransferStartedMessage but we have already created the payout tx " + + log.warn("We received a PaymentSentMessage but we have already created the payout tx " + "so we ignore the message. This can happen if the ACK message to the peer did not " + "arrive and the peer repeats sending us the message. We send another ACK msg."); sendAckMessage(peer, message, true, null); removeMailboxMessageAfterProcessing(message); })) .setup(tasks( - SellerProcessCounterCurrencyTransferStartedMessage.class, + SellerProcessesPaymentSentMessage.class, ApplyFilter.class, - getVerifyPeersFeePaymentClass()) - .using(new TradeTaskRunner(trade, - () -> { - latch.countDown(); - }, - (errorMessage) -> { - latch.countDown(); - }))) + getVerifyPeersFeePaymentClass())) .executeTasks(); - wait(latch); } } @@ -119,40 +111,34 @@ public abstract class SellerProtocol extends DisputeProtocol { public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { log.info("SellerProtocol.onPaymentReceived()"); synchronized (trade) { - CountDownLatch latch = new CountDownLatch(1); SellerEvent event = SellerEvent.PAYMENT_RECEIVED; - expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED) +// CountDownLatch latch = new CountDownLatch(1); // TODO (woodser): user countdown latch, but freezes legacy app + expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYOUT_PUBLISHED) .with(event) .preCondition(trade.confirmPermitted())) .setup(tasks( ApplyFilter.class, getVerifyPeersFeePaymentClass(), - SellerSignAndPublishPayoutTx.class, - // SellerSignAndFinalizePayoutTx.class, - // SellerBroadcastPayoutTx.class, - SellerSendPayoutTxPublishedMessage.class) + SellerPreparesPaymentReceivedMessage.class, + SellerSendsPaymentReceivedMessage.class) .using(new TradeTaskRunner(trade, () -> { - latch.countDown(); resultHandler.handleResult(); handleTaskRunnerSuccess(event); }, (errorMessage) -> { - latch.countDown(); errorMessageHandler.handleErrorMessage(errorMessage); handleTaskRunnerFault(event, errorMessage); }))) - .run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT)) + .run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT)) .executeTasks(); - wait(latch); } } - @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); - if (message instanceof CounterCurrencyTransferStartedMessage) { - handle((CounterCurrencyTransferStartedMessage) message, peer); + if (message instanceof PaymentSentMessage) { + handle((PaymentSentMessage) message, peer); } } 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 97c1c7ce..adf2e06f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -21,7 +21,7 @@ import bisq.core.offer.Offer; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.handlers.TradeResultHandler; -import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.SignContractRequest; @@ -294,10 +294,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // TODO (woodser): support notifications of ack messages private void onAckMessage(AckMessage ackMessage, NodeAddress peer) { - // We handle the ack for CounterCurrencyTransferStartedMessage and DepositTxAndDelayedPayoutTxMessage + // We handle the ack for PaymentSentMessage and DepositTxAndDelayedPayoutTxMessage // as we support automatic re-send of the msg in case it was not ACKed after a certain time // TODO (woodser): add AckMessage for InitTradeRequest and support automatic re-send ? - if (ackMessage.getSourceMsgClassName().equals(CounterCurrencyTransferStartedMessage.class.getSimpleName())) { + if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { processModel.setPaymentStartedAckMessage(ackMessage); } else if (ackMessage.getSourceMsgClassName().equals(DepositTxAndDelayedPayoutTxMessage.class.getSimpleName())) { processModel.setDepositTxSentAckMessage(ackMessage); 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 7d794e8b..37c5d9ed 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java @@ -35,7 +35,7 @@ import java.util.stream.Collectors; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; - +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 @@ -108,13 +108,17 @@ public final class TradingPeer implements PersistablePayload { @Nullable private String madeMultisigHex; @Nullable - private String signedPayoutTxHex; - @Nullable private String depositTxHash; @Nullable private String depositTxHex; @Nullable private String depositTxKey; + @Nullable + transient private MoneroTxWallet payoutTx; + @Nullable + private String payoutTxHex; + @Nullable + private String updatedMultisigHex; public TradingPeer() { } @@ -146,10 +150,11 @@ public final class TradingPeer implements PersistablePayload { Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex)); Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex)); - Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex)); + Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash)); Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); + Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); builder.setCurrentDate(currentDate); return builder.build(); @@ -189,10 +194,11 @@ public final class TradingPeer implements PersistablePayload { tradingPeer.setReserveTxKeyImages(proto.getReserveTxKeyImagesList()); tradingPeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex())); tradingPeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex())); - tradingPeer.setSignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex())); tradingPeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash())); tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); + tradingPeer.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex())); + tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); return tradingPeer; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java index fb7b698f..c792a9c8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesDepositRequest.java @@ -22,10 +22,10 @@ import bisq.common.app.Version; import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; -import bisq.core.trade.TradeUtils; import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.protocol.TradingPeer; @@ -37,7 +37,6 @@ import java.util.Date; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import monero.daemon.MoneroDaemon; -import monero.wallet.MoneroWallet; @Slf4j public class ArbitratorProcessesDepositRequest extends TradeTask { @@ -86,14 +85,12 @@ public class ArbitratorProcessesDepositRequest extends TradeTask { else throw new RuntimeException("DepositRequest is not from maker or taker"); // flush reserve tx from pool - MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); + XmrWalletService xmrWalletService = trade.getXmrWalletService(); + MoneroDaemon daemon = xmrWalletService.getDaemon(); daemon.flushTxPool(trader.getReserveTxHash()); - // process and verify deposit tx - TradeUtils.processTradeTx( - daemon, - trade.getXmrWalletService().getWallet(), - depositAddress, + // verify deposit tx + xmrWalletService.verifyTradeTx(depositAddress, depositAmount, tradeFee, trader.getDepositTxHash(), diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java index 2cc651ce..7cc579e2 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorProcessesReserveTx.java @@ -55,9 +55,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask { // process reserve tx with expected terms BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee()); BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); - TradeUtils.processTradeTx( - processModel.getXmrWalletService().getDaemon(), - processModel.getXmrWalletService().getWallet(), + trade.getXmrWalletService().verifyTradeTx( request.getPayoutAddress(), depositAmount, tradeFee, diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java index 6658a43c..66de7983 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessSignContractRequest.java @@ -27,7 +27,6 @@ import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.Trade.State; -import bisq.core.trade.TradeUtils; import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.protocol.TradingPeer; @@ -71,7 +70,7 @@ public class ProcessSignContractRequest extends TradeTask { } // create and sign contract - Contract contract = TradeUtils.createContract(trade); + Contract contract = trade.createContract(); String contractAsJson = Utilities.objectToJson(contract); String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson); 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 index 9961198e..c98b132a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessUpdateMultisigRequest.java @@ -72,9 +72,9 @@ public class ProcessUpdateMultisigRequest extends TradeTask { String updatedMultisigHex = multisigWallet.getMultisigHex(); // import the multisig hex - int numOutputsSigned = multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex())); + 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()); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java index b9628a29..e024c681 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendSignContractRequestAfterMultisig.java @@ -18,25 +18,14 @@ package bisq.core.trade.protocol.tasks; import bisq.common.app.Version; -import bisq.common.crypto.PubKeyRing; import bisq.common.taskrunner.TaskRunner; import bisq.core.btc.model.XmrAddressEntry; -import bisq.core.offer.Offer; -import bisq.core.trade.MakerTrade; -import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.TradeUtils; import bisq.core.trade.messages.SignContractRequest; -import bisq.core.trade.protocol.TradeListener; -import bisq.core.util.ParsingUtils; -import bisq.network.p2p.AckMessage; -import bisq.network.p2p.NodeAddress; import bisq.network.p2p.SendDirectMessageListener; -import java.math.BigInteger; import java.util.Date; import java.util.UUID; import lombok.extern.slf4j.Slf4j; -import monero.daemon.model.MoneroOutput; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxWallet; @@ -57,88 +46,78 @@ public class SendSignContractRequestAfterMultisig extends TradeTask { try { runInterceptHook(); - synchronized (trade.getXmrWalletService().getWallet()) { // synchronize on wallet to create deposit tx and freeze funds - - // skip if multisig wallet not complete - if (!processModel.isMultisigSetupComplete()) { - complete(); - return; // TODO: woodser: this does not ack original request? - } - - // skip if deposit tx already created - if (processModel.getDepositTxXmr() != null) { - complete(); - return; - } - - // thaw reserved outputs - MoneroWallet wallet = trade.getXmrWalletService().getWallet(); - for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) { - wallet.thawOutput(reserveTxKeyImage); - } - - // create deposit tx - BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee()); - Offer offer = processModel.getOffer(); - BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit()); - String multisigAddress = processModel.getMultisigAddress(); - MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount); - - // freeze deposit outputs - // TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig - for (MoneroOutput input : depositTx.getInputs()) { - wallet.freezeOutput(input.getKeyImage().getHex()); - } - - // save process state - processModel.setDepositTxXmr(depositTx); - trade.getSelf().setDepositTxHash(depositTx.getHash()); - - // create request for peer and arbitrator to sign contract - SignContractRequest request = new SignContractRequest( - trade.getOffer().getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - new Date().getTime(), - trade.getProcessModel().getAccountId(), - trade.getProcessModel().getPaymentAccountPayload(trade).getHash(), - trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), - depositTx.getHash()); - - // send request to trading peer - processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId()); - ack1 = true; - if (ack1 && ack2) completeAux(); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); - failed(); - } - }); - - // send request to arbitrator - processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId()); - ack2 = true; - if (ack1 && ack2) completeAux(); - } - @Override - public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); - failed(); - } - }); + // skip if multisig wallet not complete + if (!processModel.isMultisigSetupComplete()) { + complete(); + return; // TODO: woodser: this does not ack original request? } + + // skip if deposit tx already created + if (processModel.getDepositTxXmr() != null) { + complete(); + return; + } + + // thaw reserved outputs + MoneroWallet wallet = trade.getXmrWalletService().getWallet(); + for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) { + wallet.thawOutput(reserveTxKeyImage); + } + + // 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 + processModel.setDepositTxXmr(depositTx); + trade.getSelf().setDepositTxHash(depositTx.getHash()); + trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address? + + // create request for peer and arbitrator to sign contract + SignContractRequest request = new SignContractRequest( + trade.getOffer().getId(), + processModel.getMyNodeAddress(), + processModel.getPubKeyRing(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + new Date().getTime(), + trade.getProcessModel().getAccountId(), + trade.getProcessModel().getPaymentAccountPayload(trade).getHash(), + trade.getSelf().getPayoutAddressString(), + depositTx.getHash()); + + // send request to trading peer + processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId()); + ack1 = true; + if (ack1 && ack2) completeAux(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + }); + + // send request to arbitrator + processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId()); + ack2 = true; + if (ack1 && ack2) completeAux(); + } + @Override + public void onFault(String errorMessage) { + log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage); + failed(); + } + }); } catch (Throwable t) { failed(t); } 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 index 39719cc7..5d86335d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/UpdateMultisigWithTradingPeer.java @@ -57,7 +57,7 @@ public class UpdateMultisigWithTradingPeer extends TradeTask { // fetch relevant trade info XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerCreateAndSignPayoutTx + MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerPreparesPaymentStartedMessage // skip if multisig wallet does not need updated if (!multisigWallet.isMultisigImportNeeded()) { @@ -72,8 +72,8 @@ public class UpdateMultisigWithTradingPeer extends TradeTask { public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) { if (!(message instanceof UpdateMultisigResponse)) return; UpdateMultisigResponse response = (UpdateMultisigResponse) message; - multisigWallet.importMultisigHex(Arrays.asList(response.getUpdatedMultisigHex())); multisigWallet.sync(); + multisigWallet.importMultisigHex(response.getUpdatedMultisigHex()); trade.removeListener(updateMultisigResponseListener); complete(); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java similarity index 61% rename from core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java index 59eb1070..bb0a97ee 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerCreateAndSignPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerPreparesPaymentSentMessage.java @@ -18,10 +18,8 @@ package bisq.core.trade.protocol.tasks.buyer; import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; -import bisq.core.util.ParsingUtils; import bisq.common.taskrunner.TaskRunner; @@ -38,18 +36,16 @@ import static com.google.common.base.Preconditions.checkNotNull; -import monero.common.MoneroError; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroAccount; import monero.wallet.model.MoneroSubaddress; -import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; @Slf4j -public class BuyerCreateAndSignPayoutTx extends TradeTask { +public class BuyerPreparesPaymentSentMessage extends TradeTask { @SuppressWarnings({"unused"}) - public BuyerCreateAndSignPayoutTx(TaskRunner taskHandler, Trade trade) { + public BuyerPreparesPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -57,61 +53,36 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask { protected void run() { try { runInterceptHook(); - + // validate state Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null"); Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null"); checkNotNull(trade.getOffer(), "offer must not be null"); - - // gather relevant trade info + + // get multisig wallet XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); - String sellerPayoutAddress = trade.getTradingPeer().getPayoutAddressString(); - String buyerPayoutAddress = trade instanceof MakerTrade ? trade.getContract().getMakerPayoutAddressString() : trade.getContract().getTakerPayoutAddressString(); - Preconditions.checkNotNull(sellerPayoutAddress, "sellerPayoutAddress must not be null"); - Preconditions.checkNotNull(buyerPayoutAddress, "buyerPayoutAddress must not be null"); - BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount(); - BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount(); - BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount()); - BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); - BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); - // create transaction to get fee estimate - if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Multisig import is still needed!!!"); - MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig() - .setAccountIndex(0) - .addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx - .addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) - .setRelay(false) - ); - - // attempt to create payout tx by increasing estimated fee until successful - MoneroTxWallet payoutTx = null; - int numAttempts = 0; - while (payoutTx == null && numAttempts < 50) { - BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10 of fee until tx is successful - try { - numAttempts++; - payoutTx = multisigWallet.createTx(new MoneroTxConfig() - .setAccountIndex(0) - .addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount - .addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) - .setRelay(false)); - } catch (MoneroError e) { - // exception expected - } + // create payout tx if we have seller's updated multisig hex + if (!multisigWallet.isMultisigImportNeeded()) { + log.info("Buyer creating unsigned payout tx"); + MoneroTxWallet payoutTx = trade.createPayoutTx(); + trade.getBuyer().setPayoutTx(payoutTx); + trade.getBuyer().setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } else { + trade.getSelf().setUpdatedMultisigHex(multisigWallet.getMultisigHex()); } - - if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts"); - log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx); - processModel.setBuyerSignedPayoutTx(payoutTx); + + // close multisig wallet walletService.closeMultisigWallet(trade.getId()); complete(); } catch (Throwable t) { failed(t); } } + + // TODO (woodser): move these to gen utils /** * Generic parameterized pair. diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessesPaymentReceivedMessage.java similarity index 60% rename from core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessesPaymentReceivedMessage.java index 080498d4..b11b1bd7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessesPaymentReceivedMessage.java @@ -20,7 +20,7 @@ package bisq.core.trade.protocol.tasks.buyer; import bisq.core.account.sign.SignedWitness; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.util.Validator; @@ -38,8 +38,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import monero.wallet.MoneroWallet; @Slf4j -public class BuyerProcessPayoutTxPublishedMessage extends TradeTask { - public BuyerProcessPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { +public class BuyerProcessesPaymentReceivedMessage extends TradeTask { + public BuyerProcessesPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -48,23 +48,34 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask { try { runInterceptHook(); log.debug("current trade state " + trade.getState()); - PayoutTxPublishedMessage message = (PayoutTxPublishedMessage) processModel.getTradeMessage(); + PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage(); Validator.checkTradeId(processModel.getOfferId(), message); checkNotNull(message); - checkArgument(message.getSignedMultisigTxHex() != null); + checkArgument(message.getPayoutTxHex() != null); // update to the latest peer address of our peer if the message is correct trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); - + + // handle if payout tx is not seen on network if (trade.getPayoutTx() == null) { - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); - List txHashes = multisigWallet.submitMultisigTxHex(message.getSignedMultisigTxHex()); - trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0))); - XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx()); - trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG); - walletService.closeMultisigWallet(trade.getId()); - //processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId()); + + // publish payout tx if signed. otherwise verify, sign, and publish payout tx + boolean fullySigned = trade.getSelf().getPayoutTx() != null; + if (fullySigned) { + 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.setState(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.verifySignAndPublishPayoutTx(message.getPayoutTxHex()); + trade.setState(Trade.State.BUYER_PUBLISHED_PAYOUT_TX); + // TODO (woodser): send PayoutTxPublishedMessage to arbitrator and seller + } } else { log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java similarity index 88% rename from core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java index 41cab18d..9a3d8f71 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsPaymentSentMessage.java @@ -21,7 +21,7 @@ import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.network.MessageState; import bisq.core.trade.Trade; -import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.TradeMailboxMessage; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; @@ -35,9 +35,10 @@ import javafx.beans.value.ChangeListener; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; +import monero.wallet.MoneroWallet; /** - * We send the seller the BuyerSendCounterCurrencyTransferStartedMessage. + * We send the seller the BuyerSendPaymentSentMessage. * We wait to receive a ACK message back and resend the message * in case that does not happen in 10 minutes or if the message was stored in mailbox or failed. We keep repeating that * with doubling the interval each time and until the MAX_RESEND_ATTEMPTS is reached. @@ -46,15 +47,15 @@ import lombok.extern.slf4j.Slf4j; * online he will process it. */ @Slf4j -public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxMessageTask { +public class BuyerSendsPaymentSentMessage extends SendMailboxMessageTask { private static final int MAX_RESEND_ATTEMPTS = 10; private int delayInMin = 15; private int resendCounter = 0; - private CounterCurrencyTransferStartedMessage message; + private PaymentSentMessage message; private ChangeListener listener; private Timer timer; - public BuyerSendCounterCurrencyTransferStartedMessage(TaskRunner taskHandler, Trade trade) { + public BuyerSendsPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -66,21 +67,21 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); final String id = processModel.getOfferId(); XmrAddressEntry payoutAddressEntry = walletService.getOrCreateAddressEntry(id, XmrAddressEntry.Context.TRADE_PAYOUT); - String payoutTxHex = processModel.getBuyerSignedPayoutTx().getTxSet().getMultisigTxHex(); // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox // 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 CounterCurrencyTransferStartedMessage( + message = new PaymentSentMessage( tradeId, payoutAddressEntry.getAddressString(), processModel.getMyNodeAddress(), - payoutTxHex, trade.getCounterCurrencyTxId(), trade.getCounterCurrencyExtraData(), - deterministicId + deterministicId, + trade.getBuyer().getPayoutTxHex(), + trade.getBuyer().getUpdatedMultisigHex() ); } return message; @@ -88,8 +89,8 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM @Override protected void setStateSent() { - if (trade.getState().ordinal() < Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG.ordinal()) { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG); + if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_INITIATED_MSG.ordinal()) { + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_INITIATED_MSG); } processModel.getTradeManager().requestPersistence(); @@ -111,7 +112,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM @Override protected void setStateStoredInMailbox() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG); if (!trade.isPayoutPublished()) { tryToSendAgainLater(); } @@ -126,7 +127,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM @Override protected void setStateFault() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG); if (!trade.isPayoutPublished()) { tryToSendAgainLater(); } @@ -165,7 +166,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM private void tryToSendAgainLater() { if (resendCounter >= MAX_RESEND_ATTEMPTS) { cleanup(); - log.warn("We never received an ACK message when sending the CounterCurrencyTransferStartedMessage to the peer. " + + log.warn("We never received an ACK message when sending the PaymentSentMessage to the peer. " + "We stop now and complete the protocol task."); complete(); return; @@ -192,7 +193,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM // Once we receive an ACK from our msg we know the peer has received the msg and we stop. if (newValue == MessageState.ACKNOWLEDGED) { // We treat a ACK like BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG); processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPreparesPaymentReceivedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPreparesPaymentReceivedMessage.java new file mode 100644 index 00000000..beb9d038 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPreparesPaymentReceivedMessage.java @@ -0,0 +1,59 @@ +/* + * 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.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import monero.wallet.model.MoneroTxWallet; + +@Slf4j +public class SellerPreparesPaymentReceivedMessage extends TradeTask { + + @SuppressWarnings({"unused"}) + public SellerPreparesPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // verify, sign, and publish payout tx if given. otherwise create payout tx + if (trade.getBuyer().getPayoutTxHex() != null) { + log.info("Seller verifying, signing, and publishing payout tx"); + trade.verifySignAndPublishPayoutTx(trade.getBuyer().getPayoutTxHex()); + } else { + log.info("Seller creating unsigned payout tx"); + MoneroTxWallet payoutTx = trade.createPayoutTx(); + System.out.println("created payout tx: " + payoutTx); + trade.getSeller().setPayoutTx(payoutTx); + trade.getSeller().setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessesPaymentSentMessage.java similarity index 62% rename from core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessesPaymentSentMessage.java index b8dbeca3..f694c158 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessesPaymentSentMessage.java @@ -17,20 +17,21 @@ package bisq.core.trade.protocol.tasks.seller; -import bisq.core.trade.Trade; -import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; -import bisq.core.trade.protocol.tasks.TradeTask; -import bisq.core.util.Validator; - -import bisq.common.taskrunner.TaskRunner; - -import lombok.extern.slf4j.Slf4j; - 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.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; +import java.util.Arrays; +import lombok.extern.slf4j.Slf4j; +import monero.wallet.MoneroWallet; + @Slf4j -public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTask { - public SellerProcessCounterCurrencyTransferStartedMessage(TaskRunner taskHandler, Trade trade) { +public class SellerProcessesPaymentSentMessage extends TradeTask { + public SellerProcessesPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -39,12 +40,21 @@ public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTas try { runInterceptHook(); log.debug("current trade state " + trade.getState()); - CounterCurrencyTransferStartedMessage message = (CounterCurrencyTransferStartedMessage) processModel.getTradeMessage(); + PaymentSentMessage message = (PaymentSentMessage) processModel.getTradeMessage(); Validator.checkTradeId(processModel.getOfferId(), message); checkNotNull(message); - trade.getTradingPeer().setPayoutAddressString(Validator.nonEmptyStringOf(message.getBuyerPayoutAddress())); // TODO (woodser): verify against contract - trade.getTradingPeer().setSignedPayoutTxHex(message.getBuyerPayoutTxSigned()); + trade.getBuyer().setPayoutAddressString(Validator.nonEmptyStringOf(message.getBuyerPayoutAddress())); // TODO (woodser): verify against contract + trade.getBuyer().setPayoutTxHex(message.getPayoutTxHex()); + trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + + // 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()); + } // update to the latest peer address of our peer if the message is correct // TODO (woodser): update to latest peer addresses where needed trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); @@ -59,7 +69,7 @@ public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTas trade.setCounterCurrencyExtraData(counterCurrencyExtraData); } - trade.setState(Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG); + trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_INITIATED_MSG); processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsPaymentReceivedMessage.java similarity index 76% rename from core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsPaymentReceivedMessage.java index 3a093656..79e2c543 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsPaymentReceivedMessage.java @@ -20,7 +20,7 @@ package bisq.core.trade.protocol.tasks.seller; import bisq.core.account.sign.SignedWitness; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayoutTxPublishedMessage; +import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.TradeMailboxMessage; import bisq.core.trade.protocol.tasks.SendMailboxMessageTask; @@ -33,71 +33,21 @@ import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode(callSuper = true) @Slf4j -public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask { +public class SellerSendsPaymentReceivedMessage extends SendMailboxMessageTask { SignedWitness signedWitness = null; - public SellerSendPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + public SellerSendsPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } - @Override - protected TradeMailboxMessage getTradeMailboxMessage(String id) { - checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null"); - - AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService(); - if (accountAgeWitnessService.isSignWitnessTrade(trade)) { - // Broadcast is done in accountAgeWitness domain. - accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness); - } - - return new PayoutTxPublishedMessage( - id, - trade.getPayoutTx().getTxSet().getMultisigTxHex(), - processModel.getMyNodeAddress(), - signedWitness - ); - } - - @Override - protected void setStateSent() { - trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG); - log.info("Sent PayoutTxPublishedMessage: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); - processModel.getTradeManager().requestPersistence(); - } - - @Override - protected void setStateArrived() { - trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG); - log.info("PayoutTxPublishedMessage arrived: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); - processModel.getTradeManager().requestPersistence(); - } - - @Override - protected void setStateStoredInMailbox() { - trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG); - log.info("PayoutTxPublishedMessage storedInMailbox: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); - processModel.getTradeManager().requestPersistence(); - } - - @Override - protected void setStateFault() { - trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG); - log.error("PayoutTxPublishedMessage failed: tradeId={} at peer {} SignedWitness {}", - trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); - processModel.getTradeManager().requestPersistence(); - } - @Override protected void run() { try { runInterceptHook(); - if (trade.getPayoutTx() == null) { - log.error("PayoutTx is null"); - failed("PayoutTx is null"); + if (trade.getSeller().getPayoutTxHex() == null) { + log.error("Payout tx is null"); + failed("Payout tx is null"); return; } @@ -106,4 +56,54 @@ public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask { failed(t); } } + + @Override + protected TradeMailboxMessage getTradeMailboxMessage(String id) { + checkNotNull(trade.getSeller().getPayoutTxHex(), "Payout tx must not be null"); + + 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, + processModel.getMyNodeAddress(), + signedWitness, + trade.getSeller().getPayoutTxHex() + ); + } + + @Override + protected void setStateSent() { + trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG); + log.info("Sent SellerReceivedPaymentMessage: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateArrived() { + trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG); + log.info("SellerReceivedPaymentMessage arrived: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG); + log.info("SellerReceivedPaymentMessage storedInMailbox: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } + + @Override + protected void setStateFault() { + trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG); + log.error("SellerReceivedPaymentMessage failed: tradeId={} at peer {} SignedWitness {}", + trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness); + processModel.getTradeManager().requestPersistence(); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java deleted file mode 100644 index d690dd25..00000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndPublishPayoutTx.java +++ /dev/null @@ -1,117 +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.seller; - -import bisq.core.btc.wallet.XmrWalletService; -import bisq.core.trade.Contract; -import bisq.core.trade.MakerTrade; -import bisq.core.trade.Trade; -import bisq.core.trade.protocol.tasks.TradeTask; -import bisq.core.util.ParsingUtils; - -import bisq.common.taskrunner.TaskRunner; - -import java.math.BigInteger; - -import lombok.extern.slf4j.Slf4j; - -import monero.wallet.MoneroWallet; -import monero.wallet.model.MoneroDestination; -import monero.wallet.model.MoneroMultisigSignResult; -import monero.wallet.model.MoneroTxSet; -import monero.wallet.model.MoneroTxWallet; - -@Slf4j -public class SellerSignAndPublishPayoutTx extends TradeTask { - - @SuppressWarnings({"unused"}) - public SellerSignAndPublishPayoutTx(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // gather relevant trade info - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); - String buyerSignedPayoutTxHex = trade.getTradingPeer().getSignedPayoutTxHex(); - Contract contract = trade.getContract(); - BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs trade.getDepositTxId() necessary or avoidable? - BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount(); - BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount()); - - // parse buyer-signed payout tx - MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(buyerSignedPayoutTxHex)); - if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): test nack - MoneroTxWallet buyerSignedPayoutTx = parsedTxSet.getTxs().get(0); - - // verify payout tx has exactly 2 destinations - log.info("Seller verifying buyer-signed payout tx"); - if (buyerSignedPayoutTx.getOutgoingTransfer() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Buyer-signed payout tx does not have exactly two destinations"); - - // get buyer and seller destinations (order not preserved) - boolean buyerFirst = buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); - MoneroDestination buyerPayoutDestination = buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1); - MoneroDestination sellerPayoutDestination = buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0); - - // verify payout addresses - if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract"); - if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract"); - - // verify change address is multisig's primary address - if (!buyerSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address"); - - // verify sum of outputs = destination amounts + change amount - if (!buyerSignedPayoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(buyerSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); - - // verify buyer destination amount is deposit amount + trade amount - 1/2 tx costs - BigInteger txCost = buyerSignedPayoutTx.getFee().add(buyerSignedPayoutTx.getChangeAmount()); - BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2))); - if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout); - - // verify seller destination amount is deposit amount - trade amount - 1/2 tx costs - BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2))); - if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); - - // TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx) - - // sign buyer-signed payout tx - MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(buyerSignedPayoutTxHex); - if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing buyer-signed payout tx"); - String signedMultisigTxHex = result.getSignedMultisigTxHex(); - - // submit fully signed payout tx to the network - multisigWallet.submitMultisigTxHex(signedMultisigTxHex); - - // close multisig wallet - walletService.closeMultisigWallet(trade.getId()); - - // update trade state - parsedTxSet.setMultisigTxHex(signedMultisigTxHex); // TODO (woodser): better place to store this? - trade.setPayoutTx(parsedTxSet.getTxs().get(0)); - trade.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash()); - trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); - complete(); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java index a3007b85..1fb9e5a0 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerReservesTradeFunds.java @@ -20,14 +20,12 @@ package bisq.core.trade.protocol.tasks.taker; import bisq.common.taskrunner.TaskRunner; import bisq.core.btc.model.XmrAddressEntry; import bisq.core.trade.Trade; -import bisq.core.trade.TradeUtils; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.util.ParsingUtils; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import monero.daemon.model.MoneroOutput; -import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxWallet; public class TakerReservesTradeFunds extends TradeTask { @@ -41,27 +39,23 @@ public class TakerReservesTradeFunds extends TradeTask { try { runInterceptHook(); - // synchronize on wallet to reserve key images - synchronized (model.getXmrWalletService().getWallet()) { - - // create transaction to reserve trade - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); - BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong()); - MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount); - - // collect reserved key images // TODO (woodser): switch to proof of reserve? - List reservedKeyImages = new ArrayList(); - for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - - // save process state - // TODO (woodser): persist - processModel.setReserveTx(reserveTx); - processModel.getTaker().setReserveTxKeyImages(reservedKeyImages); - trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used? - //trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states - complete(); - } + // freeze trade funds and get reserve tx + String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); + BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong()); + MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, returnAddress, depositAmount); + + // collect reserved key images // TODO (woodser): switch to proof of reserve? + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + + // save process state + // TODO (woodser): persist + processModel.setReserveTx(reserveTx); + processModel.getTaker().setReserveTxKeyImages(reservedKeyImages); + trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used? + //trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states + complete(); } catch (Throwable t) { trade.setErrorMessage("An error occurred.\n" + "Error message:\n" diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index 7dc4447d..82445756 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -346,7 +346,7 @@ public class XmrTxProofService implements AssetTxProofService { } private boolean isExpectedTradeState(Trade.State newValue) { - return newValue == Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; + return newValue == Trade.State.SELLER_RECEIVED_PAYMENT_INITIATED_MSG; } private boolean is32BitHexStringInValid(String hexString) { 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 1b212dc0..fe8c0afd 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -28,11 +28,11 @@ import bisq.core.offer.placeoffer.tasks.MakerReservesTradeFunds; import bisq.core.offer.placeoffer.tasks.ValidateOffer; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; -import bisq.core.trade.protocol.tasks.buyer.BuyerCreateAndSignPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerPreparesPaymentSentMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDepositTxAndDelayedPayoutTxMessage; -import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage; -import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessesPaymentReceivedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendsPaymentSentMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse; import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx; @@ -48,13 +48,13 @@ import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx; import bisq.core.trade.protocol.tasks.seller.SellerFinalizesDelayedPayoutTx; -import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerProcessesPaymentSentMessage; import bisq.core.trade.protocol.tasks.seller.SellerProcessDelayedPayoutTxSignatureResponse; import bisq.core.trade.protocol.tasks.seller.SellerPublishesDepositTx; import bisq.core.trade.protocol.tasks.seller.SellerPublishesTradeStatistics; import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; -import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; -import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerSendsPaymentReceivedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerPreparesPaymentReceivedMessage; import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx; import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx; import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepositTx; @@ -135,15 +135,15 @@ public class DebugView extends InitializableView { SellerPublishesDepositTx.class, SellerPublishesTradeStatistics.class, - SellerProcessCounterCurrencyTransferStartedMessage.class, + SellerProcessesPaymentSentMessage.class, ApplyFilter.class, TakerVerifyMakerFeePayment.class, ApplyFilter.class, TakerVerifyMakerFeePayment.class, - SellerSignAndPublishPayoutTx.class, + SellerPreparesPaymentReceivedMessage.class, //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? - SellerSendPayoutTxPublishedMessage.class + SellerSendsPaymentReceivedMessage.class ) )); @@ -167,11 +167,11 @@ public class DebugView extends InitializableView { ApplyFilter.class, MakerVerifyTakerFeePayment.class, - BuyerCreateAndSignPayoutTx.class, + BuyerPreparesPaymentSentMessage.class, BuyerSetupPayoutTxListener.class, - BuyerSendCounterCurrencyTransferStartedMessage.class, + BuyerSendsPaymentSentMessage.class, - BuyerProcessPayoutTxPublishedMessage.class + BuyerProcessesPaymentReceivedMessage.class ) )); @@ -199,11 +199,11 @@ public class DebugView extends InitializableView { ApplyFilter.class, TakerVerifyMakerFeePayment.class, - BuyerCreateAndSignPayoutTx.class, + BuyerPreparesPaymentSentMessage.class, BuyerSetupPayoutTxListener.class, - BuyerSendCounterCurrencyTransferStartedMessage.class, + BuyerSendsPaymentSentMessage.class, - BuyerProcessPayoutTxPublishedMessage.class) + BuyerProcessesPaymentReceivedMessage.class) )); addGroup("SellerAsMakerProtocol", FXCollections.observableArrayList(Arrays.asList( @@ -227,15 +227,15 @@ public class DebugView extends InitializableView { SellerPublishesDepositTx.class, SellerPublishesTradeStatistics.class, - SellerProcessCounterCurrencyTransferStartedMessage.class, + SellerProcessesPaymentSentMessage.class, ApplyFilter.class, MakerVerifyTakerFeePayment.class, ApplyFilter.class, MakerVerifyTakerFeePayment.class, - SellerSignAndPublishPayoutTx.class, + SellerPreparesPaymentReceivedMessage.class, //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? - SellerSendPayoutTxPublishedMessage.class + SellerSendsPaymentReceivedMessage.class ) )); } 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 37cfc0a5..1eb02c81 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 @@ -409,8 +409,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im appendMsg = Res.get("takeOffer.error.feePaid"); break; case DEPOSIT_PUBLISHED: - case FIAT_SENT: - case FIAT_RECEIVED: + case PAYMENT_SENT: + case PAYMENT_RECEIVED: appendMsg = Res.get("takeOffer.error.depositPublished"); break; case PAYOUT_PUBLISHED: 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 0ac26980..d90214f1 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 @@ -194,7 +194,7 @@ public class NotificationCenter { if (trade instanceof BuyerTrade && phase.ordinal() == Trade.Phase.DEPOSIT_CONFIRMED.ordinal()) message = Res.get("notification.trade.confirmed"); - else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.FIAT_SENT.ordinal()) + else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.PAYMENT_SENT.ordinal()) message = Res.get("notification.trade.paymentStarted"); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 03d4a008..4ff1fa32 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -468,27 +468,33 @@ public class PendingTradesView extends ActivatableViewAndModel { - if (trade.isPayoutPublished()) { - if (chatPopupStage.isShowing()) { - chatPopupStage.hide(); + UserThread.execute(() -> { + if (trade.isPayoutPublished()) { + if (chatPopupStage.isShowing()) { + chatPopupStage.hide(); + } } - } + }); }; trade.stateProperty().addListener(tradeStateListener); disputeStateListener = (observable, oldValue, newValue) -> { - if (newValue == Trade.DisputeState.DISPUTE_CLOSED || newValue == Trade.DisputeState.REFUND_REQUEST_CLOSED) { - chatPopupStage.hide(); - } + UserThread.execute(() -> { + if (newValue == Trade.DisputeState.DISPUTE_CLOSED || newValue == Trade.DisputeState.REFUND_REQUEST_CLOSED) { + chatPopupStage.hide(); + } + }); }; trade.disputeStateProperty().addListener(disputeStateListener); mediationResultStateListener = (observable, oldValue, newValue) -> { - if (newValue == MediationResultState.PAYOUT_TX_PUBLISHED || - newValue == MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG || - newValue == MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK) { - chatPopupStage.hide(); - } + UserThread.execute(() -> { + if (newValue == MediationResultState.PAYOUT_TX_PUBLISHED || + newValue == MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG || + newValue == MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK) { + chatPopupStage.hide(); + } + }); }; trade.mediationResultStateProperty().addListener(mediationResultStateListener); @@ -559,21 +565,23 @@ public class PendingTradesView extends ActivatableViewAndModel 0) { - badge.setText(String.valueOf(num)); - badge.setEnabled(true); + UserThread.execute(() -> { + if (!trade.getId().equals(tradeIdOfOpenChat)) { + updateNewChatMessagesByTradeMap(); + long num = newChatMessagesByTradeMap.get(trade.getId()); + if (num > 0) { + badge.setText(String.valueOf(num)); + badge.setEnabled(true); + } else { + badge.setText(""); + badge.setEnabled(false); + } } else { badge.setText(""); badge.setEnabled(false); } - } else { - badge.setText(""); - badge.setEnabled(false); - } - badge.refreshBadge(); + badge.refreshBadge(); + }); } /////////////////////////////////////////////////////////////////////////////////////////// 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 3a9bb988..a773eec2 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 @@ -452,27 +452,27 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { switch (state) { - case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED: - case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG: + case BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED: + case BUYER_SENT_PAYMENT_INITIATED_MSG: busyAnimation.play(); statusLabel.setText(Res.get("shared.sendingConfirmation")); model.setMessageStateProperty(MessageState.SENT); @@ -149,17 +149,17 @@ public class BuyerStep2View extends TradeStepView { statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); }, 10); break; - case BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG: + case BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageArrived")); model.setMessageStateProperty(MessageState.ARRIVED); break; - case BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG: + case BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageStoredInMailbox")); model.setMessageStateProperty(MessageState.STORED_IN_MAILBOX); break; - case BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG: + case BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG: // We get a popup and the trade closed, so we dont need to show anything here busyAnimation.stop(); statusLabel.setText(""); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 70863709..caca3311 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -116,7 +116,7 @@ public class SellerStep3View extends TradeStepView { } else if (trade.isFiatReceived()) { if (!trade.hasFailed()) { switch (state) { - case SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT: + case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: case SELLER_PUBLISHED_PAYOUT_TX: case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG: busyAnimation.play(); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 736885fc..1ce66615 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -39,34 +39,32 @@ message NetworkEnvelope { InputsForDepositTxRequest inputs_for_deposit_tx_request = 17; InputsForDepositTxResponse inputs_for_deposit_tx_response = 18; DepositTxMessage deposit_tx_message = 19; - CounterCurrencyTransferStartedMessage counter_currency_transfer_started_message = 20; - PayoutTxPublishedMessage payout_tx_published_message = 21; - OpenNewDisputeMessage open_new_dispute_message = 22; - PeerOpenedDisputeMessage peer_opened_dispute_message = 23; - ChatMessage chat_message = 24; - DisputeResultMessage dispute_result_message = 25; - PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 26; + OpenNewDisputeMessage open_new_dispute_message = 20; + PeerOpenedDisputeMessage peer_opened_dispute_message = 21; + ChatMessage chat_message = 22; + DisputeResultMessage dispute_result_message = 23; + PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 24; - PrivateNotificationMessage private_notification_message = 27; + PrivateNotificationMessage private_notification_message = 25; - AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 28; - AckMessage ack_message = 29; + AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 26; + AckMessage ack_message = 27; - BundleOfEnvelopes bundle_of_envelopes = 30; - MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 31; - MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 32; + BundleOfEnvelopes bundle_of_envelopes = 28; + MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 29; + MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 30; - DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 33; - DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 34; - DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 35; - PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 36; + DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 31; + DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 32; + DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 33; + PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 34; - RefreshTradeStateRequest refresh_trade_state_request = 37 [deprecated = true]; - TraderSignedWitnessMessage trader_signed_witness_message = 38 [deprecated = true]; + RefreshTradeStateRequest refresh_trade_state_request = 35 [deprecated = true]; + TraderSignedWitnessMessage trader_signed_witness_message = 36 [deprecated = true]; - GetInventoryRequest get_inventory_request = 39; - GetInventoryResponse get_inventory_response = 40; + GetInventoryRequest get_inventory_request = 37; + GetInventoryResponse get_inventory_response = 38; SignOfferRequest sign_offer_request = 1001; SignOfferResponse sign_offer_response = 1002; @@ -77,10 +75,13 @@ message NetworkEnvelope { DepositRequest deposit_request = 1007; DepositResponse deposit_response = 1008; PaymentAccountPayloadRequest payment_account_payload_request = 1009; - UpdateMultisigRequest update_multisig_request = 1010; - UpdateMultisigResponse update_multisig_response = 1011; - ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012; - ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013; + PaymentSentMessage payment_sent_message = 1010; + PaymentReceivedMessage payment_received_message = 1011; + PayoutTxPublishedMessage payout_tx_published_message = 1012; + UpdateMultisigRequest update_multisig_request = 1013; + UpdateMultisigResponse update_multisig_response = 1014; + ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1015; + ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1016; } } @@ -428,16 +429,6 @@ message PeerPublishedDelayedPayoutTxMessage { NodeAddress sender_node_address = 3; } -message CounterCurrencyTransferStartedMessage { - string trade_id = 1; - string buyer_payout_address = 2; - NodeAddress sender_node_address = 3; - string buyer_payout_tx_signed = 4; - string counter_currency_tx_id = 5; - string uid = 6; - string counter_currency_extra_data = 7; -} - message FinalizePayoutTxRequest { string trade_id = 1; bytes seller_signature = 2; @@ -446,6 +437,33 @@ message FinalizePayoutTxRequest { string uid = 5; } +message PaymentSentMessage { + string trade_id = 1; + string buyer_payout_address = 2; + NodeAddress sender_node_address = 3; + string counter_currency_tx_id = 4; + string uid = 5; + string counter_currency_extra_data = 6; + string payout_tx_hex = 7; + string updated_multisig_hex = 8; +} + +message PaymentReceivedMessage { + string trade_id = 1; + 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; + string uid = 3; + SignedWitness signed_witness = 4; // Added in v1.4.0 + string payout_tx_hex = 5; +} + message ArbitratorPayoutTxRequest { Dispute dispute = 1; // TODO (woodser): replace with trade id NodeAddress sender_node_address = 2; @@ -462,14 +480,6 @@ message ArbitratorPayoutTxResponse { string arbitrator_signed_payout_tx_hex = 5; } -message PayoutTxPublishedMessage { - string trade_id = 1; - string signed_multisig_tx_hex = 2; - NodeAddress sender_node_address = 3; - string uid = 4; - SignedWitness signed_witness = 5; // Added in v1.4.0 -} - message MediatedPayoutTxPublishedMessage { string trade_id = 1; bytes payout_tx = 2; @@ -1514,13 +1524,13 @@ message Trade { MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 16; MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 17; DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 18; - BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 19; - BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 20; - BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 21; - BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 22; - BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 23; - SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 24; - SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 25; + BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED = 19; + BUYER_SENT_PAYMENT_INITIATED_MSG = 20; + BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG = 21; + BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG = 22; + BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG = 23; + SELLER_RECEIVED_PAYMENT_INITIATED_MSG = 24; + SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 25; SELLER_PUBLISHED_PAYOUT_TX = 26; SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 27; SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 28; @@ -1528,7 +1538,8 @@ message Trade { SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 30; BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 31; BUYER_SAW_PAYOUT_TX_IN_NETWORK = 32; - WITHDRAW_COMPLETED = 33; + BUYER_PUBLISHED_PAYOUT_TX = 33; + WITHDRAW_COMPLETED = 34; } enum Phase { @@ -1537,8 +1548,8 @@ message Trade { TAKER_FEE_PUBLISHED = 2; DEPOSIT_PUBLISHED = 3; DEPOSIT_CONFIRMED = 4; - FIAT_SENT = 5; - FIAT_RECEIVED = 6; + PAYMENT_SENT = 5; + PAYMENT_RECEIVED = 6; PAYOUT_PUBLISHED = 7; WITHDRAWN = 8; } @@ -1686,10 +1697,11 @@ message TradingPeer { repeated string reserve_tx_key_images = 1004; string prepared_multisig_hex = 1005; string made_multisig_hex = 1006; - string signed_payout_tx_hex = 1007; + string payout_tx_hex = 1007; string deposit_tx_hash = 1008; string deposit_tx_hex = 1009; string deposit_tx_key = 1010; + string updated_multisig_hex = 1011; } ///////////////////////////////////////////////////////////////////////////////////////////