diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index a7cc1845..73516a02 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -210,23 +210,23 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.getPrice(); } - public void verifyTakersTradePrice(long takersTradePrice) throws TradePriceOutOfToleranceException, + public void verifyTradePrice(long price) throws TradePriceOutOfToleranceException, MarketPriceNotAvailableException, IllegalArgumentException { if (!isUseMarketBasedPrice()) { - checkArgument(takersTradePrice == getFixedPrice(), + checkArgument(price == getFixedPrice(), "Takers price does not match offer price. " + - "Takers price=" + takersTradePrice + "; offer price=" + getFixedPrice()); + "Takers price=" + price + "; offer price=" + getFixedPrice()); return; } - Price tradePrice = Price.valueOf(getCurrencyCode(), takersTradePrice); + Price tradePrice = Price.valueOf(getCurrencyCode(), price); Price offerPrice = getPrice(); if (offerPrice == null) throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available."); - checkArgument(takersTradePrice > 0, "takersTradePrice must be positive"); + checkArgument(price > 0, "takersTradePrice must be positive"); - double relation = (double) takersTradePrice / (double) offerPrice.getValue(); + double relation = (double) price / (double) offerPrice.getValue(); // We allow max. 2 % difference between own offerPayload price calculation and takers calculation. // Market price might be different at maker's and takers side so we need a bit of tolerance. // The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations @@ -234,7 +234,7 @@ public class Offer implements NetworkPayload, PersistablePayload { double deviation = Math.abs(1 - relation); log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}", - getShortId(), getCurrencyCode(), takersTradePrice, offerPrice.getValue(), + getShortId(), getCurrencyCode(), price, offerPrice.getValue(), deviation * 100 + "%"); if (deviation > PRICE_TOLERANCE) { String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" + diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index c37bf505..a5496552 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1392,7 +1392,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference // in trade price between the peers. Also here poor connectivity might cause market price API connection // losses and therefore an outdated market price. - offer.verifyTakersTradePrice(request.getTakersTradePrice()); + offer.verifyTradePrice(request.getTakersTradePrice()); availabilityResult = AvailabilityResult.AVAILABLE; } catch (TradePriceOutOfToleranceException e) { log.warn("Trade price check failed because takers price is outside out tolerance."); diff --git a/core/src/main/java/haveno/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java b/core/src/main/java/haveno/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java index 9ffd6ea3..d5bc5814 100644 --- a/core/src/main/java/haveno/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java +++ b/core/src/main/java/haveno/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java @@ -23,7 +23,6 @@ import haveno.core.offer.AvailabilityResult; import haveno.core.offer.Offer; import haveno.core.offer.availability.OfferAvailabilityModel; import haveno.core.offer.messages.OfferAvailabilityResponse; -import haveno.core.trade.HavenoUtils; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; @@ -52,13 +51,6 @@ public class ProcessOfferAvailabilityResponse extends Task { User user = model.getUser(); P2PService p2PService = model.getP2PService(); XmrWalletService walletService = model.getXmrWalletService(); - String paymentAccountId = model.getPaymentAccountId(); - String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId(); + String makerPaymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId(); + String takerPaymentAccountId = model.getPaymentAccountId(); + String paymentMethodId = user.getPaymentAccount(takerPaymentAccountId).getPaymentAccountPayload().getPaymentMethodId(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // taker signs offer using offer id as nonce to avoid challenge protocol @@ -66,14 +68,16 @@ public class SendOfferAvailabilityRequest extends Task { // send InitTradeRequest to maker to sign InitTradeRequest tradeRequest = new InitTradeRequest( + TradeProtocolVersion.MULTISIG_2_3, // TODO: replace with first of their accepted protocols offer.getId(), - P2PService.getMyNodeAddress(), - p2PService.getKeyRing().getPubKeyRing(), model.getTradeAmount().longValueExact(), price.getValue(), - user.getAccountId(), - paymentAccountId, paymentMethodId, + null, + user.getAccountId(), + makerPaymentAccountId, + takerPaymentAccountId, + p2PService.getKeyRing().getPubKeyRing(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), sig, @@ -84,8 +88,7 @@ public class SendOfferAvailabilityRequest extends Task { null, // reserve tx not sent from taker to maker null, null, - payoutAddress, - null); + payoutAddress); // save trade request to later send to arbitrator model.setTradeRequest(tradeRequest); diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index ba631876..07ebeb59 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -32,7 +32,6 @@ import haveno.core.app.HavenoSetup; import haveno.core.offer.OfferPayload; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; -import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.util.JsonUtil; @@ -326,52 +325,6 @@ public class HavenoUtils { return isSignatureValid(arbitrator.getPubKeyRing(), offer.getSignatureHash(), offer.getArbitratorSignature()); } - /** - * Check if the maker signature for a trade request is valid. - * - * @param request is the trade request to check - * @return true if the maker's signature is valid for the trade request - */ - public static boolean isMakerSignatureValid(InitTradeRequest request, byte[] signature, PubKeyRing makerPubKeyRing) { - - // re-create trade request with signed fields - InitTradeRequest signedRequest = new InitTradeRequest( - request.getOfferId(), - request.getSenderNodeAddress(), - request.getPubKeyRing(), - request.getTradeAmount(), - request.getTradePrice(), - request.getAccountId(), - request.getPaymentAccountId(), - request.getPaymentMethodId(), - request.getUid(), - request.getMessageVersion(), - request.getAccountAgeWitnessSignatureOfOfferId(), - request.getCurrentDate(), - request.getMakerNodeAddress(), - request.getTakerNodeAddress(), - null, - null, - null, - null, - request.getPayoutAddress(), - null - ); - - // get trade request as string - String tradeRequestAsJson = JsonUtil.objectToJson(signedRequest); - - // verify maker signature - boolean isSignatureValid = isSignatureValid(makerPubKeyRing, tradeRequestAsJson, signature); - if (!isSignatureValid) { - log.warn("Invalid maker signature for trade request: " + request.getOfferId() + " from " + request.getSenderNodeAddress().getAddressForDisplay()); - log.warn("Trade request as json: " + tradeRequestAsJson); - log.warn("Maker pub key ring: " + (makerPubKeyRing == null ? null : "...")); - log.warn("Maker signature: " + (signature == null ? null : Utilities.bytesAsHexString(signature))); - } - return isSignatureValid; - } - /** * Verify the buyer signature for a PaymentSentMessage. * diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index cc04b781..ea3743df 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -362,7 +362,7 @@ public abstract class Trade implements Tradable, Model { private long takeOfferDate; // Initialization - private static final int TOTAL_INIT_STEPS = 23; // total estimated steps + private static final int TOTAL_INIT_STEPS = 24; // total estimated steps private int initStep = 0; @Getter private double initProgress = 0; @@ -1552,6 +1552,7 @@ public abstract class Trade implements Tradable, Model { public void addInitProgressStep() { startProtocolTimeout(); initProgress = Math.min(1.0, (double) ++initStep / TOTAL_INIT_STEPS); + //if (this instanceof TakerTrade) log.warn("Init step count: " + initStep); // log init step count for taker trades in order to update total steps UserThread.execute(() -> initProgressProperty.set(initProgress)); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 525ef2e9..69f0e6e9 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -538,182 +538,195 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { - log.info("Received InitTradeRequest from {} with tradeId {} and uid {}", sender, request.getOfferId(), request.getUid()); + log.info("Received InitTradeRequest from {} with tradeId {} and uid {}", sender, request.getOfferId(), request.getUid()); - try { - Validator.nonEmptyStringOf(request.getOfferId()); - } catch (Throwable t) { - log.warn("Invalid InitTradeRequest message " + request.toString()); - return; - } - - // handle request as arbitrator - boolean isArbitrator = request.getArbitratorNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress()); - if (isArbitrator) { - - // verify this node is registered arbitrator - Arbitrator thisArbitrator = user.getRegisteredArbitrator(); - NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); - if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because we are not an arbitrator", sender, request.getOfferId()); + try { + Validator.nonEmptyStringOf(request.getOfferId()); + } catch (Throwable t) { + log.warn("Invalid InitTradeRequest message " + request.toString()); return; } - // get offer associated with trade - Offer offer = null; - for (Offer anOffer : offerBookService.getOffers()) { - if (anOffer.getId().equals(request.getOfferId())) { - offer = anOffer; - } - } - if (offer == null) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because offer is not on the books", sender, request.getOfferId()); - return; - } + // handle request as maker + if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { - // verify arbitrator is payload signer unless they are offline - // TODO (woodser): handle if payload signer differs from current arbitrator (verify signer is offline) - - // verify maker is offer owner - // TODO (woodser): maker address might change if they disconnect and reconnect, should allow maker address to differ if pubKeyRing is same? - if (!offer.getOwnerNodeAddress().equals(request.getMakerNodeAddress())) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because maker is not offer owner", sender, request.getOfferId()); - return; - } - - // handle trade - Trade trade; - Optional tradeOptional = getOpenTrade(offer.getId()); - if (tradeOptional.isPresent()) { - trade = tradeOptional.get(); - - // verify request is from maker - if (!sender.equals(request.getMakerNodeAddress())) { - - // send nack if trade already taken - String errMsg = "Trade is already taken, tradeId=" + request.getOfferId(); - log.warn(errMsg); - sendAckMessage(sender, request.getPubKeyRing(), request, false, errMsg); + // get open offer + Optional openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId()); + if (!openOfferOptional.isPresent()) return; + OpenOffer openOffer = openOfferOptional.get(); + if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; + Offer offer = openOffer.getOffer(); + + // ensure trade does not already exist + Optional tradeOptional = getOpenTrade(request.getOfferId()); + if (tradeOptional.isPresent()) { + log.warn("Maker trade already exists with id " + request.getOfferId() + ". This should never happen."); return; } - } else { - - // verify request is from taker - if (!sender.equals(request.getTakerNodeAddress())) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request must be from taker when trade is not initialized", sender, request.getOfferId()); - return; - } - - // create arbitrator trade - trade = new ArbitratorTrade(offer, - BigInteger.valueOf(request.getTradeAmount()), - offer.getOfferPayload().getPrice(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - request.getMakerNodeAddress(), - request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); - - // set reserve tx hash if available - Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); - if (signedOfferOptional.isPresent()) { - SignedOffer signedOffer = signedOfferOptional.get(); - trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash()); - } - - // initialize trade protocol + + // reserve open offer + openOfferManager.reserveOpenOffer(openOffer); + + // initialize trade + Trade trade; + if (offer.isBuyOffer()) + trade = new BuyerAsMakerTrade(offer, + BigInteger.valueOf(request.getTradeAmount()), + offer.getOfferPayload().getPrice(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + request.getMakerNodeAddress(), + request.getTakerNodeAddress(), + request.getArbitratorNodeAddress()); + else + trade = new SellerAsMakerTrade(offer, + BigInteger.valueOf(request.getTradeAmount()), + offer.getOfferPayload().getPrice(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + request.getMakerNodeAddress(), + request.getTakerNodeAddress(), + request.getArbitratorNodeAddress()); + trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId()); + trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); + trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); + trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); + trade.getSelf().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); + trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol? + trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex()); + trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey()); + trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages()); initTradeAndProtocol(trade, createTradeProtocol(trade)); addTrade(trade); + + // notify on phase changes + // TODO (woodser): save subscription, bind on startup + EasyBind.subscribe(trade.statePhaseProperty(), phase -> { + if (phase == Phase.DEPOSITS_PUBLISHED) { + notificationService.sendTradeNotification(trade, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation + } + }); + + // process with protocol + ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { + log.warn("Maker error during trade initialization: " + errorMessage); + trade.onProtocolError(); + }); } - // process with protocol - ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { - log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); - trade.onProtocolError(); - }); + // handle request as arbitrator + else if (request.getArbitratorNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { - requestPersistence(); - } + // verify this node is registered arbitrator + Arbitrator thisArbitrator = user.getRegisteredArbitrator(); + NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); + if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) { + log.warn("Ignoring InitTradeRequest because we are not an arbitrator, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } - // handle request as maker - else { + // get offer associated with trade + Offer offer = null; + for (Offer anOffer : offerBookService.getOffers()) { + if (anOffer.getId().equals(request.getOfferId())) { + offer = anOffer; + } + } + if (offer == null) { + log.warn("Ignoring InitTradeRequest to arbitrator because offer is not on the books, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } - Optional openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId()); - if (!openOfferOptional.isPresent()) { - return; - } + // verify arbitrator is payload signer unless they are offline + // TODO (woodser): handle if payload signer differs from current arbitrator (verify signer is offline) - OpenOffer openOffer = openOfferOptional.get(); - if (openOffer.getState() != OpenOffer.State.AVAILABLE) { - return; - } + // verify maker is offer owner + // TODO (woodser): maker address might change if they disconnect and reconnect, should allow maker address to differ if pubKeyRing is same? + if (!offer.getOwnerNodeAddress().equals(request.getMakerNodeAddress())) { + log.warn("Ignoring InitTradeRequest to arbitrator because maker is not offer owner, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } - Offer offer = openOffer.getOffer(); + // handle trade + Trade trade; + Optional tradeOptional = getOpenTrade(offer.getId()); + if (tradeOptional.isPresent()) { + trade = tradeOptional.get(); - // verify request is from arbitrator - Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(sender); - if (arbitrator == null) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request is not from accepted arbitrator", sender, request.getOfferId()); - return; - } + // verify request is from taker + if (!sender.equals(request.getTakerNodeAddress())) { + log.warn("Ignoring InitTradeRequest from non-taker, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } + } else { - Optional tradeOptional = getOpenTrade(request.getOfferId()); - if (tradeOptional.isPresent()) { - log.warn("Maker trade already exists with id " + request.getOfferId() + ". This should never happen."); - return; - } + // verify request is from maker + if (!sender.equals(request.getMakerNodeAddress())) { + log.warn("Ignoring InitTradeRequest to arbitrator because request must be from maker when trade is not initialized, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } - // reserve open offer - openOfferManager.reserveOpenOffer(openOffer); + // create arbitrator trade + trade = new ArbitratorTrade(offer, + BigInteger.valueOf(request.getTradeAmount()), + offer.getOfferPayload().getPrice(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + request.getMakerNodeAddress(), + request.getTakerNodeAddress(), + request.getArbitratorNodeAddress()); - // initialize trade - Trade trade; - if (offer.isBuyOffer()) - trade = new BuyerAsMakerTrade(offer, - BigInteger.valueOf(request.getTradeAmount()), - offer.getOfferPayload().getPrice(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - request.getMakerNodeAddress(), - request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); - else - trade = new SellerAsMakerTrade(offer, - BigInteger.valueOf(request.getTradeAmount()), - offer.getOfferPayload().getPrice(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - request.getMakerNodeAddress(), - request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + // set reserve tx hash if available + Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); + if (signedOfferOptional.isPresent()) { + SignedOffer signedOffer = signedOfferOptional.get(); + trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash()); + } - trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); - trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); - initTradeAndProtocol(trade, createTradeProtocol(trade)); - trade.getSelf().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); - trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol? - trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex()); - trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey()); - trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages()); - addTrade(trade); + // initialize trade protocol + initTradeAndProtocol(trade, createTradeProtocol(trade)); + addTrade(trade); + } - // notify on phase changes - // TODO (woodser): save subscription, bind on startup - EasyBind.subscribe(trade.statePhaseProperty(), phase -> { - if (phase == Phase.DEPOSITS_PUBLISHED) { - notificationService.sendTradeNotification(trade, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation - } - }); + // process with protocol + ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { + log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); + trade.onProtocolError(); + }); - // process with protocol - ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { - log.warn("Maker error during trade initialization: " + errorMessage); - trade.onProtocolError(); - }); - } + requestPersistence(); + } + + // handle request as taker + else if (request.getTakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { + + // verify request is from arbitrator + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(sender); + if (arbitrator == null) { + log.warn("Ignoring InitTradeRequest to taker because request is not from accepted arbitrator, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } + + // get trade + Optional tradeOptional = getOpenTrade(request.getOfferId()); + if (!tradeOptional.isPresent()) { + log.warn("Ignoring InitTradeRequest to taker because trade is not initialized, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } + Trade trade = tradeOptional.get(); + + // process with protocol + ((TakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender); + } + + // invalid sender + else { + log.warn("Ignoring InitTradeRequest because sender is not maker, arbitrator, or taker, tradeId={}, sender={}", request.getOfferId(), sender); + return; + } } private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress peer) { @@ -843,71 +856,58 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (amount.compareTo(offer.getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); - OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser, paymentAccountId, amount); - offer.checkOfferAvailability(model, - () -> { - if (offer.getState() == Offer.State.AVAILABLE) { - Trade trade; - if (offer.isBuyOffer()) { - trade = new SellerAsTakerTrade(offer, - amount, - model.getTradeRequest().getTradePrice(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - model.getPeerNodeAddress(), - P2PService.getMyNodeAddress(), - offer.getOfferPayload().getArbitratorSigner()); - } else { - trade = new BuyerAsTakerTrade(offer, - amount, - model.getTradeRequest().getTradePrice(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - model.getPeerNodeAddress(), - P2PService.getMyNodeAddress(), - offer.getOfferPayload().getArbitratorSigner()); - } + // ensure trade is not already open + Optional tradeOptional = getOpenTrade(offer.getId()); + if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " is already open"); - trade.getProcessModel().setTradeMessage(model.getTradeRequest()); - trade.getProcessModel().setMakerSignature(model.getMakerSignature()); - trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); - trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); - trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); - trade.getSelf().setPubKeyRing(model.getPubKeyRing()); - trade.getSelf().setPaymentAccountId(paymentAccountId); + // create trade + Trade trade; + if (offer.isBuyOffer()) { + trade = new SellerAsTakerTrade(offer, + amount, + offer.getPrice().getValue(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null); + } else { + trade = new BuyerAsTakerTrade(offer, + amount, + offer.getPrice().getValue(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null); + } - // ensure trade is not already open - Optional tradeOptional = getOpenTrade(offer.getId()); - if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + trade.getId() + " is already open"); + trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); + trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); + trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); + trade.getMaker().setPubKeyRing(offer.getPubKeyRing()); + trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing()); + trade.getSelf().setPaymentAccountId(paymentAccountId); - // initialize trade protocol - TradeProtocol tradeProtocol = createTradeProtocol(trade); - addTrade(trade); + // initialize trade protocol + TradeProtocol tradeProtocol = createTradeProtocol(trade); + addTrade(trade); - initTradeAndProtocol(trade, tradeProtocol); - trade.addInitProgressStep(); + initTradeAndProtocol(trade, tradeProtocol); + trade.addInitProgressStep(); - // process with protocol - ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { - tradeResultHandler.handleResult(trade); - requestPersistence(); - }, errorMessage -> { - log.warn("Taker error during trade initialization: " + errorMessage); - xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error - trade.onProtocolError(); - errorMessageHandler.handleErrorMessage(errorMessage); - }); - requestPersistence(); - } else { - log.warn("Cannot take offer {} because it's not available, state={}", offer.getId(), offer.getState()); - } - }, - errorMessage -> { - log.warn("Taker error during check offer availability: " + errorMessage); - errorMessageHandler.handleErrorMessage(errorMessage); - }); + // process with protocol + ((TakerProtocol) tradeProtocol).onTakeOffer(result -> { + tradeResultHandler.handleResult(trade); + requestPersistence(); + }, errorMessage -> { + log.warn("Taker error during trade initialization: " + errorMessage); + xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error + trade.onProtocolError(); + errorMessageHandler.handleErrorMessage(errorMessage); + }); requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java index 12ce4b7d..48464875 100644 --- a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java @@ -33,20 +33,19 @@ import java.util.Optional; @EqualsAndHashCode(callSuper = true) @Value public final class InitTradeRequest extends TradeMessage implements DirectMessage { - private final NodeAddress senderNodeAddress; + TradeProtocolVersion tradeProtocolVersion; private final long tradeAmount; private final long tradePrice; - private final String accountId; - private final String paymentAccountId; private final String paymentMethodId; - private final PubKeyRing pubKeyRing; - - // added in v 0.6. can be null if we trade with an older peer + @Nullable + private final String makerAccountId; + private final String takerAccountId; + private final String makerPaymentAccountId; + private final String takerPaymentAccountId; + private final PubKeyRing takerPubKeyRing; @Nullable private final byte[] accountAgeWitnessSignatureOfOfferId; private final long currentDate; - - // XMR integration private final NodeAddress makerNodeAddress; private final NodeAddress takerNodeAddress; @Nullable @@ -59,36 +58,37 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag private final String reserveTxKey; @Nullable private final String payoutAddress; - @Nullable - private final byte[] makerSignature; - public InitTradeRequest(String offerId, - NodeAddress senderNodeAddress, - PubKeyRing pubKeyRing, - long tradeAmount, - long tradePrice, - String accountId, - String paymentAccountId, - String paymentMethodId, - String uid, - String messageVersion, - @Nullable byte[] accountAgeWitnessSignatureOfOfferId, - long currentDate, - NodeAddress makerNodeAddress, - NodeAddress takerNodeAddress, - NodeAddress arbitratorNodeAddress, - @Nullable String reserveTxHash, - @Nullable String reserveTxHex, - @Nullable String reserveTxKey, - @Nullable String payoutAddress, - @Nullable byte[] makerSignature) { + public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion, + String offerId, + long tradeAmount, + long tradePrice, + String paymentMethodId, + @Nullable String makerAccountId, + String takerAccountId, + String makerPaymentAccountId, + String takerPaymentAccountId, + PubKeyRing takerPubKeyRing, + String uid, + String messageVersion, + @Nullable byte[] accountAgeWitnessSignatureOfOfferId, + long currentDate, + NodeAddress makerNodeAddress, + NodeAddress takerNodeAddress, + NodeAddress arbitratorNodeAddress, + @Nullable String reserveTxHash, + @Nullable String reserveTxHex, + @Nullable String reserveTxKey, + @Nullable String payoutAddress) { super(messageVersion, offerId, uid); - this.senderNodeAddress = senderNodeAddress; - this.pubKeyRing = pubKeyRing; + this.tradeProtocolVersion = tradeProtocolVersion; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; - this.accountId = accountId; - this.paymentAccountId = paymentAccountId; + this.makerAccountId = makerAccountId; + this.takerAccountId = takerAccountId; + this.makerPaymentAccountId = makerPaymentAccountId; + this.takerPaymentAccountId = takerPaymentAccountId; + this.takerPubKeyRing = takerPubKeyRing; this.paymentMethodId = paymentMethodId; this.accountAgeWitnessSignatureOfOfferId = accountAgeWitnessSignatureOfOfferId; this.currentDate = currentDate; @@ -99,7 +99,6 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; this.payoutAddress = payoutAddress; - this.makerSignature = makerSignature; } @@ -107,28 +106,30 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - @Override + + @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.InitTradeRequest.Builder builder = protobuf.InitTradeRequest.newBuilder() + .setTradeProtocolVersion(TradeProtocolVersion.toProtoMessage(tradeProtocolVersion)) .setOfferId(offerId) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setTakerNodeAddress(takerNodeAddress.toProtoMessage()) .setMakerNodeAddress(makerNodeAddress.toProtoMessage()) .setTradeAmount(tradeAmount) .setTradePrice(tradePrice) - .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setPaymentAccountId(paymentAccountId) + .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) + .setMakerPaymentAccountId(makerPaymentAccountId) + .setTakerPaymentAccountId(takerPaymentAccountId) .setPaymentMethodId(paymentMethodId) - .setAccountId(accountId) + .setTakerAccountId(takerAccountId) .setUid(uid); + Optional.ofNullable(makerAccountId).ifPresent(e -> builder.setMakerAccountId(makerAccountId)); Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())); Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress)); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); - Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder().setInitTradeRequest(builder).build(); @@ -137,14 +138,16 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag public static InitTradeRequest fromProto(protobuf.InitTradeRequest proto, CoreProtoResolver coreProtoResolver, String messageVersion) { - return new InitTradeRequest(proto.getOfferId(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), - PubKeyRing.fromProto(proto.getPubKeyRing()), + return new InitTradeRequest(TradeProtocolVersion.fromProto(proto.getTradeProtocolVersion()), + proto.getOfferId(), proto.getTradeAmount(), proto.getTradePrice(), - proto.getAccountId(), - proto.getPaymentAccountId(), proto.getPaymentMethodId(), + ProtoUtil.stringOrNullFromProto(proto.getMakerAccountId()), + proto.getTakerAccountId(), + proto.getMakerPaymentAccountId(), + proto.getTakerPaymentAccountId(), + PubKeyRing.fromProto(proto.getTakerPubKeyRing()), proto.getUid(), messageVersion, ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()), @@ -155,29 +158,31 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), - ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()), - ProtoUtil.byteArrayOrNullFromProto(proto.getMakerSignature())); + ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress())); } @Override public String toString() { return "InitTradeRequest{" + - "\n senderNodeAddress=" + senderNodeAddress + + "\n tradeProtocolVersion=" + tradeProtocolVersion + ",\n offerId=" + offerId + ",\n tradeAmount=" + tradeAmount + ",\n tradePrice=" + tradePrice + - ",\n pubKeyRing=" + pubKeyRing + - ",\n accountId='" + accountId + '\'' + - ",\n paymentAccountId=" + paymentAccountId + ",\n paymentMethodId=" + paymentMethodId + - ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + + ",\n makerAccountId=" + makerAccountId + + ",\n takerAccountId=" + takerAccountId + + ",\n makerPaymentAccountId=" + makerPaymentAccountId + + ",\n takerPaymentAccountId=" + takerPaymentAccountId + + ",\n takerPubKeyRing=" + takerPubKeyRing + ",\n accountAgeWitnessSignatureOfOfferId=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfOfferId) + ",\n currentDate=" + currentDate + + ",\n makerNodeAddress=" + makerNodeAddress + + ",\n takerNodeAddress=" + takerNodeAddress + + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n reserveTxHash=" + reserveTxHash + ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKey=" + reserveTxKey + ",\n payoutAddress=" + payoutAddress + - ",\n makerSignature=" + (makerSignature == null ? null : Utilities.byteArrayToInteger(makerSignature)) + "\n} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/messages/TradeProtocolVersion.java b/core/src/main/java/haveno/core/trade/messages/TradeProtocolVersion.java new file mode 100644 index 00000000..1dd50d5b --- /dev/null +++ b/core/src/main/java/haveno/core/trade/messages/TradeProtocolVersion.java @@ -0,0 +1,33 @@ +/* + * 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 haveno.core.trade.messages; + +import haveno.common.proto.ProtoUtil; + +public enum TradeProtocolVersion { + MULTISIG_2_3; + + public static TradeProtocolVersion fromProto( + protobuf.TradeProtocolVersion tradeProtocolVersion) { + return ProtoUtil.enumFromProto(TradeProtocolVersion.class, tradeProtocolVersion.name()); + } + + public static protobuf.TradeProtocolVersion toProtoMessage(TradeProtocolVersion tradeProtocolVersion) { + return protobuf.TradeProtocolVersion.valueOf(tradeProtocolVersion.name()); + } +} \ No newline at end of file diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java index 80ca4cf2..c88433f8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java @@ -40,7 +40,7 @@ import haveno.core.trade.BuyerAsMakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; -import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequest; +import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @@ -71,7 +71,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, - MakerSendInitTradeRequest.class) + MakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java index bc683734..d20a531b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java @@ -40,9 +40,13 @@ import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.BuyerAsTakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.handlers.TradeResultHandler; +import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; +import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; +import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToMaker; +import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -64,31 +68,60 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".onTakeOffer()"); - ThreadUtils.execute(() -> { - synchronized (trade) { - latchTrade(); - this.tradeResultHandler = tradeResultHandler; - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(TakerEvent.TAKE_OFFER) - .from(trade.getTradePeer().getNodeAddress())) - .setup(tasks( - ApplyFilter.class, - TakerReserveTradeFunds.class, - TakerSendInitTradeRequestToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(); - unlatchTrade(); - }, - errorMessage -> { - handleError(errorMessage); - })) - .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + ThreadUtils.execute(() -> { + synchronized (trade) { + latchTrade(); + this.tradeResultHandler = tradeResultHandler; + this.errorMessageHandler = errorMessageHandler; + expect(phase(Trade.Phase.INIT) + .with(TakerEvent.TAKE_OFFER) + .from(trade.getTradePeer().getNodeAddress())) + .setup(tasks( + ApplyFilter.class, + TakerReserveTradeFunds.class, + TakerSendInitTradeRequestToMaker.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + unlatchTrade(); + }, + errorMessage -> { + handleError(errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); + } + + @Override + public void handleInitTradeRequest(InitTradeRequest message, + NodeAddress peer) { + System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + ThreadUtils.execute(() -> { + synchronized (trade) { + latchTrade(); + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessInitTradeRequest.class, + TakerSendInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + handleTaskRunnerFault(peer, message, errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java index e6a8ec0b..3f30901f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java @@ -41,7 +41,7 @@ import haveno.core.trade.SellerAsMakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; -import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequest; +import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @@ -76,7 +76,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc .setup(tasks( ApplyFilter.class, ProcessInitTradeRequest.class, - MakerSendInitTradeRequest.class) + MakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { startTimeout(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java index c0290008..75076bd3 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java @@ -40,12 +40,15 @@ import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.SellerAsTakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.handlers.TradeResultHandler; +import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.protocol.tasks.ApplyFilter; +import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest; import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; +import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToMaker; +import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; -// TODO (woodser): remove unused request handling @Slf4j public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtocol { @@ -65,31 +68,60 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); - ThreadUtils.execute(() -> { - synchronized (trade) { - latchTrade(); - this.tradeResultHandler = tradeResultHandler; - this.errorMessageHandler = errorMessageHandler; - expect(phase(Trade.Phase.INIT) - .with(TakerEvent.TAKE_OFFER) - .from(trade.getTradePeer().getNodeAddress())) - .setup(tasks( - ApplyFilter.class, - TakerReserveTradeFunds.class, - TakerSendInitTradeRequestToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - startTimeout(); - unlatchTrade(); - }, - errorMessage -> { - handleError(errorMessage); - })) - .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) - .executeTasks(true); - awaitTradeLatch(); - } - }, trade.getId()); + System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + ThreadUtils.execute(() -> { + synchronized (trade) { + latchTrade(); + this.tradeResultHandler = tradeResultHandler; + this.errorMessageHandler = errorMessageHandler; + expect(phase(Trade.Phase.INIT) + .with(TakerEvent.TAKE_OFFER) + .from(trade.getTradePeer().getNodeAddress())) + .setup(tasks( + ApplyFilter.class, + TakerReserveTradeFunds.class, + TakerSendInitTradeRequestToMaker.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + unlatchTrade(); + }, + errorMessage -> { + handleError(errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); + } + + @Override + public void handleInitTradeRequest(InitTradeRequest message, + NodeAddress peer) { + System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + ThreadUtils.execute(() -> { + synchronized (trade) { + latchTrade(); + expect(phase(Trade.Phase.INIT) + .with(message) + .from(peer)) + .setup(tasks( + ApplyFilter.class, + ProcessInitTradeRequest.class, + TakerSendInitTradeRequestToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + startTimeout(); + handleTaskRunnerSuccess(peer, message); + }, + errorMessage -> { + handleTaskRunnerFault(peer, message, errorMessage); + })) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) + .executeTasks(true); + awaitTradeLatch(); + } + }, trade.getId()); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/TakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TakerProtocol.java index 2617fc93..ca863eb3 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TakerProtocol.java @@ -19,9 +19,12 @@ package haveno.core.trade.protocol; import haveno.common.handlers.ErrorMessageHandler; import haveno.core.trade.handlers.TradeResultHandler; +import haveno.core.trade.messages.InitTradeRequest; +import haveno.network.p2p.NodeAddress; public interface TakerProtocol extends TraderProtocol { void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler); + void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer); enum TakerEvent implements FluentProtocol.Event { TAKE_OFFER diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java index 1e4fdf6b..fff1192c 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java @@ -48,10 +48,11 @@ public class ArbitratorProcessReserveTx extends TradeTask { runInterceptHook(); Offer offer = trade.getOffer(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); - boolean isFromMaker = request.getSenderNodeAddress().equals(trade.getMaker().getNodeAddress()); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + boolean isFromMaker = sender == trade.getMaker(); boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL; - // TODO (woodser): if signer online, should never be called by maker + // TODO (woodser): if signer online, should never be called by maker? // process reserve tx with expected values BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); @@ -73,7 +74,7 @@ public class ArbitratorProcessReserveTx extends TradeTask { null); } catch (Exception e) { e.printStackTrace(); - throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); + throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); } // save reserve tx to model diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java index 53b331ae..9ec7aebe 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java @@ -24,6 +24,7 @@ import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitTradeRequest; +import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; @@ -50,20 +51,23 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { try { runInterceptHook(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); + TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); - // handle request from taker - if (request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress())) { + // handle request from maker + if (sender == trade.getMaker()) { - // create request to initialize trade with maker - InitTradeRequest makerRequest = new InitTradeRequest( + // create request to taker + InitTradeRequest takerRequest = new InitTradeRequest( + request.getTradeProtocolVersion(), processModel.getOfferId(), - request.getSenderNodeAddress(), - request.getPubKeyRing(), trade.getAmount().longValueExact(), trade.getPrice().getValue(), - request.getAccountId(), - request.getPaymentAccountId(), request.getPaymentMethodId(), + request.getMakerAccountId(), + request.getTakerAccountId(), + request.getMakerPaymentAccountId(), + request.getTakerPaymentAccountId(), + request.getTakerPubKeyRing(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), request.getAccountAgeWitnessSignatureOfOfferId(), @@ -72,35 +76,34 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { trade.getTaker().getNodeAddress(), trade.getArbitrator().getNodeAddress(), null, - null, // do not include taker's reserve tx null, null, null); - // send request to maker - log.info("Send {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); + // send request to taker + log.info("Send {} with offerId {} and uid {} to taker {}", takerRequest.getClass().getSimpleName(), takerRequest.getOfferId(), takerRequest.getUid(), trade.getTaker().getNodeAddress()); processModel.getP2PService().sendEncryptedDirectMessage( - trade.getMaker().getNodeAddress(), // TODO (woodser): maker's address might be different from original owner address if they disconnect and reconnect, need to validate and update address when requests received - trade.getMaker().getPubKeyRing(), - makerRequest, + trade.getTaker().getNodeAddress(), // TODO (woodser): maker's address might be different from original owner address if they disconnect and reconnect, need to validate and update address when requests received + trade.getTaker().getPubKeyRing(), + takerRequest, new SendDirectMessageListener() { @Override public void onArrived() { - log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid()); + log.info("{} arrived at taker: offerId={}; uid={}", takerRequest.getClass().getSimpleName(), takerRequest.getOfferId(), takerRequest.getUid()); complete(); } @Override public void onFault(String errorMessage) { - log.error("Sending {} failed: uid={}; peer={}; error={}", makerRequest.getClass().getSimpleName(), makerRequest.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage); - appendToErrorMessage("Sending message failed: message=" + makerRequest + "\nerrorMessage=" + errorMessage); + log.error("Sending {} failed: uid={}; peer={}; error={}", takerRequest.getClass().getSimpleName(), takerRequest.getUid(), trade.getTaker().getNodeAddress(), errorMessage); + appendToErrorMessage("Sending message failed: message=" + takerRequest + "\nerrorMessage=" + errorMessage); failed(); } } ); } - // handle request from maker - else if (request.getSenderNodeAddress().equals(trade.getMaker().getNodeAddress())) { + // handle request from taker + else if (sender == trade.getTaker()) { sendInitMultisigRequests(); complete(); // TODO: wait for InitMultisigRequest arrivals? } else { @@ -113,10 +116,9 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { private void sendInitMultisigRequests() { - // ensure arbitrator has maker's reserve tx - if (processModel.getMaker().getReserveTxHash() == null) { - throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade"); - } + // ensure arbitrator has reserve txs + if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade"); + if (processModel.getTaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade"); // create wallet for multisig MoneroWallet multisigWallet = trade.createWallet(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java deleted file mode 100644 index 0d31e4e6..00000000 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java +++ /dev/null @@ -1,98 +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 haveno.core.trade.protocol.tasks; - -import haveno.common.app.Version; -import haveno.common.taskrunner.TaskRunner; -import haveno.core.offer.Offer; -import haveno.core.trade.Trade; -import haveno.core.trade.messages.InitTradeRequest; -import haveno.core.xmr.model.XmrAddressEntry; -import haveno.network.p2p.SendDirectMessageListener; -import lombok.extern.slf4j.Slf4j; - -import java.util.UUID; - -import static com.google.common.base.Preconditions.checkNotNull; -import static haveno.core.util.Validator.checkTradeId; - -@Slf4j -public class MakerSendInitTradeRequest extends TradeTask { - @SuppressWarnings({"unused"}) - public MakerSendInitTradeRequest(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - - // verify trade state - InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to maker - checkNotNull(makerRequest); - checkTradeId(processModel.getOfferId(), makerRequest); - if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); - - // create request to arbitrator - Offer offer = processModel.getOffer(); - InitTradeRequest arbitratorRequest = new InitTradeRequest( - offer.getId(), - processModel.getMyNodeAddress(), - processModel.getPubKeyRing(), - trade.getAmount().longValueExact(), - trade.getPrice().getValue(), - trade.getProcessModel().getAccountId(), - offer.getMakerPaymentAccountId(), - offer.getOfferPayload().getPaymentMethodId(), - UUID.randomUUID().toString(), - Version.getP2PMessageVersion(), - null, - makerRequest.getCurrentDate(), - trade.getMaker().getNodeAddress(), - trade.getTaker().getNodeAddress(), - trade.getArbitrator().getNodeAddress(), - trade.getSelf().getReserveTxHash(), - trade.getSelf().getReserveTxHex(), - trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), - null); - - // send request to arbitrator - log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); - processModel.getP2PService().sendEncryptedDirectMessage( - trade.getArbitrator().getNodeAddress(), - trade.getArbitrator().getPubKeyRing(), - arbitratorRequest, - new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); - complete(); - } - @Override - public void onFault(String errorMessage) { - log.warn("Failed to send {} to arbitrator, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage); - failed(); - } - }); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java new file mode 100644 index 00000000..456ef449 --- /dev/null +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequestToArbitrator.java @@ -0,0 +1,153 @@ +/* + * 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 haveno.core.trade.protocol.tasks; + +import haveno.common.app.Version; +import haveno.common.handlers.ErrorMessageHandler; +import haveno.common.handlers.ResultHandler; +import haveno.common.taskrunner.TaskRunner; +import haveno.core.offer.availability.DisputeAgentSelection; +import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; +import haveno.core.trade.HavenoUtils; +import haveno.core.trade.Trade; +import haveno.core.trade.messages.InitTradeRequest; +import haveno.core.xmr.model.XmrAddressEntry; +import haveno.network.p2p.NodeAddress; +import haveno.network.p2p.SendDirectMessageListener; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashSet; +import java.util.Set; + +@Slf4j +public class MakerSendInitTradeRequestToArbitrator extends TradeTask { + + @SuppressWarnings({"unused"}) + public MakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // get least used arbitrator + Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager()); + if (leastUsedArbitrator == null) { + failed("Could not get least used arbitrator to send " + InitTradeRequest.class.getSimpleName() + " for offer " + trade.getId()); + return; + } + + // send request to least used arbitrators until success + sendInitTradeRequests(leastUsedArbitrator.getNodeAddress(), new HashSet(), () -> { + trade.addInitProgressStep(); + complete(); + }, (errorMessage) -> { + log.warn("Cannot initialize trade with arbitrators: " + errorMessage); + failed(errorMessage); + }); + } catch (Throwable t) { + failed(t); + } + } + + private void sendInitTradeRequests(NodeAddress arbitratorNodeAddress, Set excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + sendInitTradeRequest(arbitratorNodeAddress, new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); + + // check if trade still exists + if (!processModel.getTradeManager().hasOpenTrade(trade)) { + errorMessageHandler.handleErrorMessage("Trade protocol no longer exists, tradeId=" + trade.getId()); + return; + } + resultHandler.handleResult(); + } + + // if unavailable, try alternative arbitrator + @Override + public void onFault(String errorMessage) { + log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); + excludedArbitrators.add(arbitratorNodeAddress); + + // check if trade still exists + if (!processModel.getTradeManager().hasOpenTrade(trade)) { + errorMessageHandler.handleErrorMessage("Trade protocol no longer exists, tradeId=" + trade.getId()); + return; + } + + Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager(), excludedArbitrators); + if (altArbitrator == null) { + errorMessageHandler.handleErrorMessage("Cannot take offer because no arbitrators are available"); + return; + } + log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress()); + sendInitTradeRequests(altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler); + } + }); + } + + private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) { + + // get registered arbitrator + Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); + if (arbitrator == null) throw new RuntimeException("Node address " + arbitratorNodeAddress + " is not a registered arbitrator"); + + // set pub keys + processModel.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); + trade.getArbitrator().setNodeAddress(arbitratorNodeAddress); + trade.getArbitrator().setPubKeyRing(processModel.getArbitrator().getPubKeyRing()); + + // create request to arbitrator + InitTradeRequest takerRequest = (InitTradeRequest) processModel.getTradeMessage(); // taker's InitTradeRequest to maker + InitTradeRequest arbitratorRequest = new InitTradeRequest( + takerRequest.getTradeProtocolVersion(), + trade.getId(), + trade.getAmount().longValueExact(), + trade.getPrice().getValue(), + trade.getOffer().getOfferPayload().getPaymentMethodId(), + trade.getProcessModel().getAccountId(), + takerRequest.getTakerAccountId(), + trade.getOffer().getOfferPayload().getMakerPaymentAccountId(), + takerRequest.getTakerPaymentAccountId(), + trade.getTaker().getPubKeyRing(), + takerRequest.getUid(), + Version.getP2PMessageVersion(), + null, + takerRequest.getCurrentDate(), + trade.getMaker().getNodeAddress(), + trade.getTaker().getNodeAddress(), + trade.getArbitrator().getNodeAddress(), + trade.getSelf().getReserveTxHash(), + trade.getSelf().getReserveTxHex(), + trade.getSelf().getReserveTxKey(), + model.getXmrWalletService().getAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); + + // send request to arbitrator + log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); + processModel.getP2PService().sendEncryptedDirectMessage( + arbitratorNodeAddress, + arbitrator.getPubKeyRing(), + arbitratorRequest, + listener, + HavenoUtils.ARBITRATOR_ACK_TIMEOUT_SECONDS + ); + } +} diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java index e1d4ed34..61962ce1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitTradeRequest.java @@ -21,11 +21,13 @@ import com.google.common.base.Charsets; import haveno.common.taskrunner.TaskRunner; import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.offer.Offer; +import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.ArbitratorTrade; -import haveno.core.trade.HavenoUtils; import haveno.core.trade.MakerTrade; +import haveno.core.trade.TakerTrade; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; +import haveno.core.trade.messages.TradeProtocolVersion; import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; @@ -50,62 +52,31 @@ public class ProcessInitTradeRequest extends TradeTask { runInterceptHook(); Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); + + // validate checkNotNull(request); checkTradeId(processModel.getOfferId(), request); - - // validate inputs + checkArgument(request.getTradeAmount() > 0); if (trade.getAmount().compareTo(trade.getOffer().getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); if (trade.getAmount().compareTo(trade.getOffer().getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount"); - - // handle request as arbitrator - TradePeer multisigParticipant; - if (trade instanceof ArbitratorTrade) { - trade.getMaker().setPubKeyRing((trade.getOffer().getPubKeyRing())); - trade.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO (woodser): why duplicating field in process model - - // handle request from taker - if (request.getSenderNodeAddress().equals(request.getTakerNodeAddress())) { - multisigParticipant = processModel.getTaker(); - if (!trade.getTaker().getNodeAddress().equals(request.getTakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); - if (trade.getTaker().getPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest"); - trade.getTaker().setPubKeyRing(request.getPubKeyRing()); - if (!HavenoUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature - - // check trade price - try { - long tradePrice = request.getTradePrice(); - offer.verifyTakersTradePrice(tradePrice); - trade.setPrice(tradePrice); - } catch (TradePriceOutOfToleranceException e) { - failed(e.getMessage()); - } catch (Throwable e2) { - failed(e2); - } - } - - // handle request from maker - else if (request.getSenderNodeAddress().equals(request.getMakerNodeAddress())) { - multisigParticipant = processModel.getMaker(); - if (!trade.getMaker().getNodeAddress().equals(request.getMakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); // TODO (woodser): test when maker and taker do not agree, use proper handling, uninitialize trade for other takers - if (trade.getMaker().getPubKeyRing() == null) trade.getMaker().setPubKeyRing(request.getPubKeyRing()); - else if (!trade.getMaker().getPubKeyRing().equals(request.getPubKeyRing())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); // TODO (woodser): proper handling - trade.getMaker().setPubKeyRing(request.getPubKeyRing()); - if (trade.getPrice().getValue() != request.getTradePrice()) throw new RuntimeException("Maker and taker price do not agree"); - } else { - throw new RuntimeException("Sender is not trade's maker or taker"); - } - } + if (!request.getTakerNodeAddress().equals(trade.getTaker().getNodeAddress())) throw new RuntimeException("Trade's taker node address does not match request"); + if (!request.getMakerNodeAddress().equals(trade.getMaker().getNodeAddress())) throw new RuntimeException("Trade's maker node address does not match request"); + if (!request.getOfferId().equals(offer.getId())) throw new RuntimeException("Offer id does not match request's offer id"); // handle request as maker - else if (trade instanceof MakerTrade) { - multisigParticipant = processModel.getTaker(); - trade.getTaker().setNodeAddress(request.getSenderNodeAddress()); // arbitrator sends maker InitTradeRequest with taker's node address and pub key ring - trade.getTaker().setPubKeyRing(request.getPubKeyRing()); + TradePeer sender; + if (trade instanceof MakerTrade) { + sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + if (sender != trade.getTaker()) throw new RuntimeException("InitTradeRequest to maker is expected from taker"); + trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); + + // check protocol version + if (request.getTradeProtocolVersion() != TradeProtocolVersion.MULTISIG_2_3) throw new RuntimeException("Trade protocol version is not supported"); // TODO: check if contained in supported versions // check trade price try { long tradePrice = request.getTradePrice(); - offer.verifyTakersTradePrice(tradePrice); + offer.verifyTradePrice(tradePrice); trade.setPrice(tradePrice); } catch (TradePriceOutOfToleranceException e) { failed(e.getMessage()); @@ -114,27 +85,78 @@ public class ProcessInitTradeRequest extends TradeTask { } } + // handle request as arbitrator + else if (trade instanceof ArbitratorTrade) { + trade.getMaker().setPubKeyRing((trade.getOffer().getPubKeyRing())); // TODO: why initializing this here fields here and + trade.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO: why duplicating field in process model? + if (!trade.getArbitrator().getNodeAddress().equals(request.getArbitratorNodeAddress())) throw new RuntimeException("Trade's arbitrator node address does not match request"); + + // check protocol version + if (request.getTradeProtocolVersion() != TradeProtocolVersion.MULTISIG_2_3) throw new RuntimeException("Trade protocol version is not supported"); // TODO: check consistent from maker and taker when multiple protocols supported + + // handle request from maker + sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + if (sender == trade.getMaker()) { + trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing()); + + // check trade price + try { + long tradePrice = request.getTradePrice(); + offer.verifyTradePrice(tradePrice); + trade.setPrice(tradePrice); + } catch (TradePriceOutOfToleranceException e) { + failed(e.getMessage()); + } catch (Throwable e2) { + failed(e2); + } + } + + // handle request from taker + else if (sender == trade.getTaker()) { + if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring"); + if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); + if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); + } + + // handle invalid sender + else { + throw new RuntimeException("Sender is not trade's maker or taker"); + } + } + + // handle request as taker + else if (trade instanceof TakerTrade) { + if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount"); + if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price"); + Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(request.getArbitratorNodeAddress()); + if (arbitrator == null) throw new RuntimeException("Arbitrator is not accepted by taker"); + trade.getArbitrator().setNodeAddress(request.getArbitratorNodeAddress()); + trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); + sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress()); + if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator"); + } + // handle invalid trade type else { throw new RuntimeException("Invalid trade type to process init trade request: " + trade.getClass().getName()); } // set trading peer info - if (multisigParticipant.getPaymentAccountId() == null) multisigParticipant.setPaymentAccountId(request.getPaymentAccountId()); - else if (multisigParticipant.getPaymentAccountId() != request.getPaymentAccountId()) throw new RuntimeException("Payment account id is different from previous"); - multisigParticipant.setPubKeyRing(checkNotNull(request.getPubKeyRing())); - multisigParticipant.setAccountId(nonEmptyStringOf(request.getAccountId())); - multisigParticipant.setPaymentMethodId(nonEmptyStringOf(request.getPaymentMethodId())); - multisigParticipant.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); - multisigParticipant.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId()); - multisigParticipant.setCurrentDate(request.getCurrentDate()); + if (trade.getMaker().getAccountId() == null) trade.getMaker().setAccountId(request.getMakerAccountId()); + else if (!trade.getMaker().getAccountId().equals(request.getMakerAccountId())) throw new RuntimeException("Maker account id is different from previous"); + if (trade.getTaker().getAccountId() == null) trade.getTaker().setAccountId(request.getTakerAccountId()); + else if (!trade.getTaker().getAccountId().equals(request.getTakerAccountId())) throw new RuntimeException("Taker account id is different from previous"); + if (trade.getMaker().getPaymentAccountId() == null) trade.getMaker().setPaymentAccountId(request.getMakerPaymentAccountId()); + else if (!trade.getMaker().getPaymentAccountId().equals(request.getMakerPaymentAccountId())) throw new RuntimeException("Maker payment account id is different from previous"); + if (trade.getTaker().getPaymentAccountId() == null) trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId()); + else if (!trade.getTaker().getPaymentAccountId().equals(request.getTakerPaymentAccountId())) throw new RuntimeException("Taker payment account id is different from previous"); + sender.setPaymentMethodId(nonEmptyStringOf(request.getPaymentMethodId())); // TODO: move to process model? + sender.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); + sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId()); + sender.setCurrentDate(request.getCurrentDate()); // check peer's current date - processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(multisigParticipant.getCurrentDate())); - - // check trade amount - checkArgument(request.getTradeAmount() > 0); - checkArgument(request.getTradeAmount() == trade.getAmount().longValueExact(), "Trade amount does not match request's trade amount"); + processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(sender.getCurrentDate())); // persist trade trade.addInitProgressStep(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index ff78eae7..6b90c5ce 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -95,11 +95,14 @@ public class TakerReserveTradeFunds extends TradeTask { trade.startProtocolTimeout(); // update trade state + trade.getTaker().setReserveTxHash(reserveTx.getHash()); + trade.getTaker().setReserveTxHex(reserveTx.getFullHex()); + trade.getTaker().setReserveTxKey(reserveTx.getKey()); trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); } // save process state - processModel.setReserveTx(reserveTx); + processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used? processModel.getTradeManager().requestPersistence(); trade.addInitProgressStep(); complete(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java index d1639e51..76c0ab90 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToArbitrator.java @@ -18,24 +18,22 @@ package haveno.core.trade.protocol.tasks; import haveno.common.app.Version; -import haveno.common.handlers.ErrorMessageHandler; -import haveno.common.handlers.ResultHandler; import haveno.common.taskrunner.TaskRunner; -import haveno.core.offer.availability.DisputeAgentSelection; -import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; -import haveno.core.trade.HavenoUtils; +import haveno.core.offer.Offer; import haveno.core.trade.Trade; import haveno.core.trade.messages.InitTradeRequest; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.messages.TradeProtocolVersion; +import haveno.core.xmr.model.XmrAddressEntry; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; -import java.util.HashSet; -import java.util.Set; +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkNotNull; +import static haveno.core.util.Validator.checkTradeId; @Slf4j public class TakerSendInitTradeRequestToArbitrator extends TradeTask { - @SuppressWarnings({"unused"}) public TakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -46,106 +44,57 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask { try { runInterceptHook(); - // get least used arbitrator - Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager()); - if (leastUsedArbitrator == null) { - failed("Could not get least used arbitrator to send " + InitTradeRequest.class.getSimpleName() + " for offer " + trade.getId()); - return; - } + // verify trade state + InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker + checkNotNull(sourceRequest); + checkTradeId(processModel.getOfferId(), sourceRequest); + if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); - // send request to least used arbitrators until success - sendInitTradeRequests(leastUsedArbitrator.getNodeAddress(), new HashSet(), () -> { - trade.addInitProgressStep(); - complete(); - }, (errorMessage) -> { - log.warn("Cannot initialize trade with arbitrators: " + errorMessage); - failed(errorMessage); - }); + // create request to arbitrator + Offer offer = processModel.getOffer(); + InitTradeRequest arbitratorRequest = new InitTradeRequest( + TradeProtocolVersion.MULTISIG_2_3, // TODO: use processModel.getTradeProtocolVersion(), select one of maker's supported versions + offer.getId(), + trade.getAmount().longValueExact(), + trade.getPrice().getValue(), + offer.getOfferPayload().getPaymentMethodId(), + trade.getMaker().getAccountId(), + trade.getTaker().getAccountId(), + trade.getMaker().getPaymentAccountId(), + trade.getTaker().getPaymentAccountId(), + trade.getTaker().getPubKeyRing(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + null, + sourceRequest.getCurrentDate(), + trade.getMaker().getNodeAddress(), + trade.getTaker().getNodeAddress(), + trade.getArbitrator().getNodeAddress(), + trade.getSelf().getReserveTxHash(), + trade.getSelf().getReserveTxHex(), + trade.getSelf().getReserveTxKey(), + model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); + + // send request to arbitrator + log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); + processModel.getP2PService().sendEncryptedDirectMessage( + trade.getArbitrator().getNodeAddress(), + trade.getArbitrator().getPubKeyRing(), + arbitratorRequest, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); + complete(); + } + @Override + public void onFault(String errorMessage) { + log.warn("Failed to send {} to arbitrator, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage); + failed(); + } + }); } catch (Throwable t) { - failed(t); + failed(t); } } - - private void sendInitTradeRequests(NodeAddress arbitratorNodeAddress, Set excludedArbitrators, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - sendInitTradeRequest(arbitratorNodeAddress, new SendDirectMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); - - // check if trade still exists - if (!processModel.getTradeManager().hasOpenTrade(trade)) { - errorMessageHandler.handleErrorMessage("Trade protocol no longer exists, tradeId=" + trade.getId()); - return; - } - resultHandler.handleResult(); - } - - // if unavailable, try alternative arbitrator - @Override - public void onFault(String errorMessage) { - log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); - excludedArbitrators.add(arbitratorNodeAddress); - - // check if trade still exists - if (!processModel.getTradeManager().hasOpenTrade(trade)) { - errorMessageHandler.handleErrorMessage("Trade protocol no longer exists, tradeId=" + trade.getId()); - return; - } - - Arbitrator altArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager(), excludedArbitrators); - if (altArbitrator == null) { - errorMessageHandler.handleErrorMessage("Cannot take offer because no arbitrators are available"); - return; - } - log.info("Using alternative arbitrator {}", altArbitrator.getNodeAddress()); - sendInitTradeRequests(altArbitrator.getNodeAddress(), excludedArbitrators, resultHandler, errorMessageHandler); - } - }); - } - - private void sendInitTradeRequest(NodeAddress arbitratorNodeAddress, SendDirectMessageListener listener) { - - // get registered arbitrator - Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); - if (arbitrator == null) throw new RuntimeException("Node address " + arbitratorNodeAddress + " is not a registered arbitrator"); - - // set pub keys - processModel.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); - trade.getArbitrator().setNodeAddress(arbitratorNodeAddress); - trade.getArbitrator().setPubKeyRing(processModel.getArbitrator().getPubKeyRing()); - - // create request to arbitrator - InitTradeRequest makerRequest = (InitTradeRequest) processModel.getTradeMessage(); // taker's InitTradeRequest to maker - InitTradeRequest arbitratorRequest = new InitTradeRequest( - makerRequest.getOfferId(), - makerRequest.getSenderNodeAddress(), - makerRequest.getPubKeyRing(), - makerRequest.getTradeAmount(), - makerRequest.getTradePrice(), - makerRequest.getAccountId(), - makerRequest.getPaymentAccountId(), - makerRequest.getPaymentMethodId(), - makerRequest.getUid(), - Version.getP2PMessageVersion(), - makerRequest.getAccountAgeWitnessSignatureOfOfferId(), - makerRequest.getCurrentDate(), - makerRequest.getMakerNodeAddress(), - makerRequest.getTakerNodeAddress(), - trade.getArbitrator().getNodeAddress(), - processModel.getReserveTx().getHash(), - processModel.getReserveTx().getFullHex(), - processModel.getReserveTx().getKey(), - makerRequest.getPayoutAddress(), - processModel.getMakerSignature()); - - // send request to arbitrator - log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress()); - processModel.getP2PService().sendEncryptedDirectMessage( - arbitratorNodeAddress, - arbitrator.getPubKeyRing(), - arbitratorRequest, - listener, - HavenoUtils.ARBITRATOR_ACK_TIMEOUT_SECONDS - ); - } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.java new file mode 100644 index 00000000..8f2957ea --- /dev/null +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerSendInitTradeRequestToMaker.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 haveno.core.trade.protocol.tasks; + +import haveno.common.app.Version; +import haveno.common.taskrunner.TaskRunner; +import haveno.core.offer.Offer; +import haveno.core.trade.HavenoUtils; +import haveno.core.trade.Trade; +import haveno.core.trade.messages.InitTradeRequest; +import haveno.core.trade.messages.TradeProtocolVersion; +import haveno.core.user.User; +import haveno.core.xmr.model.XmrAddressEntry; +import haveno.core.xmr.wallet.XmrWalletService; +import haveno.network.p2p.P2PService; +import haveno.network.p2p.SendDirectMessageListener; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.UUID; + +@Slf4j +public class TakerSendInitTradeRequestToMaker extends TradeTask { + @SuppressWarnings({"unused"}) + public TakerSendInitTradeRequestToMaker(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // verify trade state + if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash()); + + // collect fields + Offer offer = model.getOffer(); + User user = processModel.getUser(); + P2PService p2PService = processModel.getP2PService(); + XmrWalletService walletService = model.getXmrWalletService(); + String paymentAccountId = trade.getSelf().getPaymentAccountId(); + String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId(); + String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + + // taker signs offer using offer id as nonce to avoid challenge protocol + byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId()); + + // create request to maker + InitTradeRequest makerRequest = new InitTradeRequest( + TradeProtocolVersion.MULTISIG_2_3, // TODO: use processModel.getTradeProtocolVersion(), select one of maker's supported versions + offer.getId(), + trade.getAmount().longValueExact(), + trade.getPrice().getValue(), + paymentMethodId, + null, + user.getAccountId(), + trade.getMaker().getPaymentAccountId(), + trade.getTaker().getPaymentAccountId(), + p2PService.getKeyRing().getPubKeyRing(), + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + sig, + new Date().getTime(), + offer.getMakerNodeAddress(), + P2PService.getMyNodeAddress(), + null, // maker selects arbitrator + null, // reserve tx not sent from taker to maker + null, + null, + payoutAddress); + + // send request to maker + log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); + processModel.getP2PService().sendEncryptedDirectMessage( + trade.getMaker().getNodeAddress(), + trade.getMaker().getPubKeyRing(), + makerRequest, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at maker: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId()); + complete(); + } + @Override + public void onFault(String errorMessage) { + log.warn("Failed to send {} to maker, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage); + failed(); + } + }); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 3ecd75da..78f1b52f 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -733,7 +733,7 @@ public class XmrWalletService { * The transaction is submitted to the pool then flushed without relaying. * * @param offerId id of offer to verify trade tx - * @param feeAmount amount sent to fee address + * @param tradeFeeAmount amount sent to fee address * @param feeAddress fee address * @param sendAmount amount sent to transfer address * @param sendAddress transfer address @@ -743,7 +743,7 @@ public class XmrWalletService { * @param keyImages expected key images of inputs, ignored if null * @return the verified tx */ - public MoneroTx verifyTradeTx(String offerId, BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List keyImages) { + public MoneroTx verifyTradeTx(String offerId, BigInteger tradeFeeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List keyImages) { if (txHash == null) throw new IllegalArgumentException("Cannot verify trade tx with null id"); MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); @@ -780,11 +780,11 @@ public class XmrWalletService { log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); // verify proof to fee address - BigInteger actualFee = BigInteger.ZERO; - if (feeAmount.compareTo(BigInteger.ZERO) > 0) { - MoneroCheckTx feeCheck = wallet.checkTxKey(txHash, txKey, feeAddress); - if (!feeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address"); - actualFee = feeCheck.getReceivedAmount(); + BigInteger actualTradeFee = BigInteger.ZERO; + if (tradeFeeAmount.compareTo(BigInteger.ZERO) > 0) { + MoneroCheckTx tradeFeeCheck = wallet.checkTxKey(txHash, txKey, feeAddress); + if (!tradeFeeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address"); + actualTradeFee = tradeFeeCheck.getReceivedAmount(); } // verify proof to transfer address @@ -792,15 +792,15 @@ public class XmrWalletService { if (!transferCheck.isGood()) throw new RuntimeException("Invalid proof to transfer address"); BigInteger actualSendAmount = transferCheck.getReceivedAmount(); - // verify fee amount - if (!actualFee.equals(feeAmount)) throw new RuntimeException("Invalid fee amount, expected " + feeAmount + " but was " + actualFee); + // verify trade fee amount + if (!actualTradeFee.equals(tradeFeeAmount)) throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); // verify send amount BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee()); if (!actualSendAmount.equals(expectedSendAmount)) throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee()); return tx; } catch (Exception e) { - log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage()); + log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=\n" + tx) + ": " + e.getMessage()); throw e; } finally { try { diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 5466f0f7..1b681f72 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -679,7 +679,7 @@ public class MainView extends InitializableView { }); model.getTopErrorMsg().addListener((ov, oldValue, newValue) -> { - log.warn("top level warning has been set! " + newValue); + log.warn("Top level warning: " + newValue); if (newValue != null) { new Popup().warning(newValue).show(); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 1eb6b3bd..e44f843c 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -225,26 +225,31 @@ message PrefixedSealedAndSignedMessage { string uid = 4; } +enum TradeProtocolVersion { + MULTISIG_2_3 = 0; +} + message InitTradeRequest { - string offer_id = 1; - NodeAddress sender_node_address = 2; - PubKeyRing pub_key_ring = 3; - int64 trade_amount = 4; - int64 trade_price = 5; - string account_id = 6; - string payment_account_id = 7; - string payment_method_id = 8; - string uid = 9; - bytes account_age_witness_signature_of_offer_id = 10; - int64 current_date = 11; - NodeAddress maker_node_address = 12; - NodeAddress taker_node_address = 13; - NodeAddress arbitrator_node_address = 14; - string reserve_tx_hash = 15; - string reserve_tx_hex = 16; - string reserve_tx_key = 17; - string payout_address = 18; - bytes maker_signature = 19; + TradeProtocolVersion trade_protocol_version = 1; + string offer_id = 2; + int64 trade_amount = 3; + int64 trade_price = 4; + string payment_method_id = 5; + string maker_account_id = 6; + string taker_account_id = 7; + string maker_payment_account_id = 8; + string taker_payment_account_id = 9; + PubKeyRing taker_pub_key_ring = 10; + string uid = 11; + bytes account_age_witness_signature_of_offer_id = 12; + int64 current_date = 13; + NodeAddress maker_node_address = 14; + NodeAddress taker_node_address = 15; + NodeAddress arbitrator_node_address = 16; + string reserve_tx_hash = 17; + string reserve_tx_hex = 18; + string reserve_tx_key = 19; + string payout_address = 20; } message InitMultisigRequest {