maker selects arbitrator (breaking change)

This commit is contained in:
woodser 2024-05-21 11:46:50 -04:00
parent 6df5296dcd
commit 1150d929af
25 changed files with 920 additions and 718 deletions

View file

@ -210,23 +210,23 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.getPrice(); return offerPayload.getPrice();
} }
public void verifyTakersTradePrice(long takersTradePrice) throws TradePriceOutOfToleranceException, public void verifyTradePrice(long price) throws TradePriceOutOfToleranceException,
MarketPriceNotAvailableException, IllegalArgumentException { MarketPriceNotAvailableException, IllegalArgumentException {
if (!isUseMarketBasedPrice()) { if (!isUseMarketBasedPrice()) {
checkArgument(takersTradePrice == getFixedPrice(), checkArgument(price == getFixedPrice(),
"Takers price does not match offer price. " + "Takers price does not match offer price. " +
"Takers price=" + takersTradePrice + "; offer price=" + getFixedPrice()); "Takers price=" + price + "; offer price=" + getFixedPrice());
return; return;
} }
Price tradePrice = Price.valueOf(getCurrencyCode(), takersTradePrice); Price tradePrice = Price.valueOf(getCurrencyCode(), price);
Price offerPrice = getPrice(); Price offerPrice = getPrice();
if (offerPrice == null) if (offerPrice == null)
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available."); 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. // 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. // 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 // 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); double deviation = Math.abs(1 - relation);
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}", log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
getShortId(), getCurrencyCode(), takersTradePrice, offerPrice.getValue(), getShortId(), getCurrencyCode(), price, offerPrice.getValue(),
deviation * 100 + "%"); deviation * 100 + "%");
if (deviation > PRICE_TOLERANCE) { if (deviation > PRICE_TOLERANCE) {
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" + String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +

View file

@ -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 // 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 // in trade price between the peers. Also here poor connectivity might cause market price API connection
// losses and therefore an outdated market price. // losses and therefore an outdated market price.
offer.verifyTakersTradePrice(request.getTakersTradePrice()); offer.verifyTradePrice(request.getTakersTradePrice());
availabilityResult = AvailabilityResult.AVAILABLE; availabilityResult = AvailabilityResult.AVAILABLE;
} catch (TradePriceOutOfToleranceException e) { } catch (TradePriceOutOfToleranceException e) {
log.warn("Trade price check failed because takers price is outside out tolerance."); log.warn("Trade price check failed because takers price is outside out tolerance.");

View file

@ -23,7 +23,6 @@ import haveno.core.offer.AvailabilityResult;
import haveno.core.offer.Offer; import haveno.core.offer.Offer;
import haveno.core.offer.availability.OfferAvailabilityModel; import haveno.core.offer.availability.OfferAvailabilityModel;
import haveno.core.offer.messages.OfferAvailabilityResponse; import haveno.core.offer.messages.OfferAvailabilityResponse;
import haveno.core.trade.HavenoUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
@ -52,13 +51,6 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
return; return;
} }
// verify maker signature for trade request
if (!HavenoUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) {
offer.setState(Offer.State.INVALID);
failed("Take offer attempt failed because maker signature is invalid");
return;
}
offer.setState(Offer.State.AVAILABLE); offer.setState(Offer.State.AVAILABLE);
model.setMakerSignature(offerAvailabilityResponse.getMakerSignature()); model.setMakerSignature(offerAvailabilityResponse.getMakerSignature());
checkNotNull(model.getMakerSignature()); checkNotNull(model.getMakerSignature());

View file

@ -26,6 +26,7 @@ import haveno.core.offer.availability.OfferAvailabilityModel;
import haveno.core.offer.messages.OfferAvailabilityRequest; import haveno.core.offer.messages.OfferAvailabilityRequest;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.messages.TradeProtocolVersion;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
@ -53,8 +54,9 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
User user = model.getUser(); User user = model.getUser();
P2PService p2PService = model.getP2PService(); P2PService p2PService = model.getP2PService();
XmrWalletService walletService = model.getXmrWalletService(); XmrWalletService walletService = model.getXmrWalletService();
String paymentAccountId = model.getPaymentAccountId(); String makerPaymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId();
String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId(); String takerPaymentAccountId = model.getPaymentAccountId();
String paymentMethodId = user.getPaymentAccount(takerPaymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
// taker signs offer using offer id as nonce to avoid challenge protocol // taker signs offer using offer id as nonce to avoid challenge protocol
@ -66,14 +68,16 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
// send InitTradeRequest to maker to sign // send InitTradeRequest to maker to sign
InitTradeRequest tradeRequest = new InitTradeRequest( InitTradeRequest tradeRequest = new InitTradeRequest(
TradeProtocolVersion.MULTISIG_2_3, // TODO: replace with first of their accepted protocols
offer.getId(), offer.getId(),
P2PService.getMyNodeAddress(),
p2PService.getKeyRing().getPubKeyRing(),
model.getTradeAmount().longValueExact(), model.getTradeAmount().longValueExact(),
price.getValue(), price.getValue(),
user.getAccountId(),
paymentAccountId,
paymentMethodId, paymentMethodId,
null,
user.getAccountId(),
makerPaymentAccountId,
takerPaymentAccountId,
p2PService.getKeyRing().getPubKeyRing(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
sig, sig,
@ -84,8 +88,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
null, // reserve tx not sent from taker to maker null, // reserve tx not sent from taker to maker
null, null,
null, null,
payoutAddress, payoutAddress);
null);
// save trade request to later send to arbitrator // save trade request to later send to arbitrator
model.setTradeRequest(tradeRequest); model.setTradeRequest(tradeRequest);

View file

@ -32,7 +32,6 @@ import haveno.core.app.HavenoSetup;
import haveno.core.offer.OfferPayload; import haveno.core.offer.OfferPayload;
import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; 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.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.util.JsonUtil; import haveno.core.util.JsonUtil;
@ -326,52 +325,6 @@ public class HavenoUtils {
return isSignatureValid(arbitrator.getPubKeyRing(), offer.getSignatureHash(), offer.getArbitratorSignature()); 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. * Verify the buyer signature for a PaymentSentMessage.
* *

View file

@ -362,7 +362,7 @@ public abstract class Trade implements Tradable, Model {
private long takeOfferDate; private long takeOfferDate;
// Initialization // 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; private int initStep = 0;
@Getter @Getter
private double initProgress = 0; private double initProgress = 0;
@ -1552,6 +1552,7 @@ public abstract class Trade implements Tradable, Model {
public void addInitProgressStep() { public void addInitProgressStep() {
startProtocolTimeout(); startProtocolTimeout();
initProgress = Math.min(1.0, (double) ++initStep / TOTAL_INIT_STEPS); 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)); UserThread.execute(() -> initProgressProperty.set(initProgress));
} }

View file

@ -538,182 +538,195 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { 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 { try {
Validator.nonEmptyStringOf(request.getOfferId()); Validator.nonEmptyStringOf(request.getOfferId());
} catch (Throwable t) { } catch (Throwable t) {
log.warn("Invalid InitTradeRequest message " + request.toString()); 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());
return; return;
} }
// get offer associated with trade // handle request as maker
Offer offer = null; if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) {
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;
}
// verify arbitrator is payload signer unless they are offline // get open offer
// TODO (woodser): handle if payload signer differs from current arbitrator (verify signer is offline) Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId());
if (!openOfferOptional.isPresent()) return;
// verify maker is offer owner OpenOffer openOffer = openOfferOptional.get();
// TODO (woodser): maker address might change if they disconnect and reconnect, should allow maker address to differ if pubKeyRing is same? if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
if (!offer.getOwnerNodeAddress().equals(request.getMakerNodeAddress())) { Offer offer = openOffer.getOffer();
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because maker is not offer owner", sender, request.getOfferId());
return; // ensure trade does not already exist
} Optional<Trade> tradeOptional = getOpenTrade(request.getOfferId());
if (tradeOptional.isPresent()) {
// handle trade log.warn("Maker trade already exists with id " + request.getOfferId() + ". This should never happen.");
Trade trade;
Optional<Trade> 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);
return; return;
} }
} else {
// reserve open offer
// verify request is from taker openOfferManager.reserveOpenOffer(openOffer);
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()); // initialize trade
return; Trade trade;
} if (offer.isBuyOffer())
trade = new BuyerAsMakerTrade(offer,
// create arbitrator trade BigInteger.valueOf(request.getTradeAmount()),
trade = new ArbitratorTrade(offer, offer.getOfferPayload().getPrice(),
BigInteger.valueOf(request.getTradeAmount()), xmrWalletService,
offer.getOfferPayload().getPrice(), getNewProcessModel(offer),
xmrWalletService, UUID.randomUUID().toString(),
getNewProcessModel(offer), request.getMakerNodeAddress(),
UUID.randomUUID().toString(), request.getTakerNodeAddress(),
request.getMakerNodeAddress(), request.getArbitratorNodeAddress());
request.getTakerNodeAddress(), else
request.getArbitratorNodeAddress()); trade = new SellerAsMakerTrade(offer,
BigInteger.valueOf(request.getTradeAmount()),
// set reserve tx hash if available offer.getOfferPayload().getPrice(),
Optional<SignedOffer> signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId()); xmrWalletService,
if (signedOfferOptional.isPresent()) { getNewProcessModel(offer),
SignedOffer signedOffer = signedOfferOptional.get(); UUID.randomUUID().toString(),
trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash()); request.getMakerNodeAddress(),
} request.getTakerNodeAddress(),
request.getArbitratorNodeAddress());
// initialize trade protocol 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)); initTradeAndProtocol(trade, createTradeProtocol(trade));
addTrade(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 // handle request as arbitrator
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { else if (request.getArbitratorNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) {
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
trade.onProtocolError();
});
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 // get offer associated with trade
else { 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<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId()); // verify arbitrator is payload signer unless they are offline
if (!openOfferOptional.isPresent()) { // TODO (woodser): handle if payload signer differs from current arbitrator (verify signer is offline)
return;
}
OpenOffer openOffer = openOfferOptional.get(); // verify maker is offer owner
if (openOffer.getState() != OpenOffer.State.AVAILABLE) { // TODO (woodser): maker address might change if they disconnect and reconnect, should allow maker address to differ if pubKeyRing is same?
return; 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<Trade> tradeOptional = getOpenTrade(offer.getId());
if (tradeOptional.isPresent()) {
trade = tradeOptional.get();
// verify request is from arbitrator // verify request is from taker
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(sender); if (!sender.equals(request.getTakerNodeAddress())) {
if (arbitrator == null) { log.warn("Ignoring InitTradeRequest from non-taker, tradeId={}, sender={}", request.getOfferId(), sender);
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request is not from accepted arbitrator", sender, request.getOfferId()); return;
return; }
} } else {
Optional<Trade> tradeOptional = getOpenTrade(request.getOfferId()); // verify request is from maker
if (tradeOptional.isPresent()) { if (!sender.equals(request.getMakerNodeAddress())) {
log.warn("Maker trade already exists with id " + request.getOfferId() + ". This should never happen."); log.warn("Ignoring InitTradeRequest to arbitrator because request must be from maker when trade is not initialized, tradeId={}, sender={}", request.getOfferId(), sender);
return; return;
} }
// reserve open offer // create arbitrator trade
openOfferManager.reserveOpenOffer(openOffer); 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 // set reserve tx hash if available
Trade trade; Optional<SignedOffer> signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId());
if (offer.isBuyOffer()) if (signedOfferOptional.isPresent()) {
trade = new BuyerAsMakerTrade(offer, SignedOffer signedOffer = signedOfferOptional.get();
BigInteger.valueOf(request.getTradeAmount()), trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash());
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.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); // initialize trade protocol
trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); initTradeAndProtocol(trade, createTradeProtocol(trade));
initTradeAndProtocol(trade, createTradeProtocol(trade)); addTrade(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);
// notify on phase changes // process with protocol
// TODO (woodser): save subscription, bind on startup ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
EasyBind.subscribe(trade.statePhaseProperty(), phase -> { log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
if (phase == Phase.DEPOSITS_PUBLISHED) { trade.onProtocolError();
notificationService.sendTradeNotification(trade, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation });
}
});
// process with protocol requestPersistence();
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { }
log.warn("Maker error during trade initialization: " + errorMessage);
trade.onProtocolError(); // 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<Trade> 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) { 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.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"); if (amount.compareTo(offer.getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount");
OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser, paymentAccountId, amount); // ensure trade is not already open
offer.checkOfferAvailability(model, Optional<Trade> tradeOptional = getOpenTrade(offer.getId());
() -> { if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + offer.getId() + " is already open");
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());
}
trade.getProcessModel().setTradeMessage(model.getTradeRequest()); // create trade
trade.getProcessModel().setMakerSignature(model.getMakerSignature()); Trade trade;
trade.getProcessModel().setUseSavingsWallet(useSavingsWallet); if (offer.isBuyOffer()) {
trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact()); trade = new SellerAsTakerTrade(offer,
trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); amount,
trade.getSelf().setPubKeyRing(model.getPubKeyRing()); offer.getPrice().getValue(),
trade.getSelf().setPaymentAccountId(paymentAccountId); 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 trade.getProcessModel().setUseSavingsWallet(useSavingsWallet);
Optional<Trade> tradeOptional = getOpenTrade(offer.getId()); trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact());
if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + trade.getId() + " is already open"); trade.getMaker().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId());
trade.getMaker().setPubKeyRing(offer.getPubKeyRing());
trade.getSelf().setPubKeyRing(keyRing.getPubKeyRing());
trade.getSelf().setPaymentAccountId(paymentAccountId);
// initialize trade protocol // initialize trade protocol
TradeProtocol tradeProtocol = createTradeProtocol(trade); TradeProtocol tradeProtocol = createTradeProtocol(trade);
addTrade(trade); addTrade(trade);
initTradeAndProtocol(trade, tradeProtocol); initTradeAndProtocol(trade, tradeProtocol);
trade.addInitProgressStep(); trade.addInitProgressStep();
// process with protocol // process with protocol
((TakerProtocol) tradeProtocol).onTakeOffer(result -> { ((TakerProtocol) tradeProtocol).onTakeOffer(result -> {
tradeResultHandler.handleResult(trade); tradeResultHandler.handleResult(trade);
requestPersistence(); requestPersistence();
}, errorMessage -> { }, errorMessage -> {
log.warn("Taker error during trade initialization: " + errorMessage); log.warn("Taker error during trade initialization: " + errorMessage);
xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error
trade.onProtocolError(); trade.onProtocolError();
errorMessageHandler.handleErrorMessage(errorMessage); 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);
});
requestPersistence(); requestPersistence();
} }

View file

@ -33,20 +33,19 @@ import java.util.Optional;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Value
public final class InitTradeRequest extends TradeMessage implements DirectMessage { public final class InitTradeRequest extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress; TradeProtocolVersion tradeProtocolVersion;
private final long tradeAmount; private final long tradeAmount;
private final long tradePrice; private final long tradePrice;
private final String accountId;
private final String paymentAccountId;
private final String paymentMethodId; private final String paymentMethodId;
private final PubKeyRing pubKeyRing; @Nullable
private final String makerAccountId;
// added in v 0.6. can be null if we trade with an older peer private final String takerAccountId;
private final String makerPaymentAccountId;
private final String takerPaymentAccountId;
private final PubKeyRing takerPubKeyRing;
@Nullable @Nullable
private final byte[] accountAgeWitnessSignatureOfOfferId; private final byte[] accountAgeWitnessSignatureOfOfferId;
private final long currentDate; private final long currentDate;
// XMR integration
private final NodeAddress makerNodeAddress; private final NodeAddress makerNodeAddress;
private final NodeAddress takerNodeAddress; private final NodeAddress takerNodeAddress;
@Nullable @Nullable
@ -59,36 +58,37 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
private final String reserveTxKey; private final String reserveTxKey;
@Nullable @Nullable
private final String payoutAddress; private final String payoutAddress;
@Nullable
private final byte[] makerSignature;
public InitTradeRequest(String offerId, public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion,
NodeAddress senderNodeAddress, String offerId,
PubKeyRing pubKeyRing, long tradeAmount,
long tradeAmount, long tradePrice,
long tradePrice, String paymentMethodId,
String accountId, @Nullable String makerAccountId,
String paymentAccountId, String takerAccountId,
String paymentMethodId, String makerPaymentAccountId,
String uid, String takerPaymentAccountId,
String messageVersion, PubKeyRing takerPubKeyRing,
@Nullable byte[] accountAgeWitnessSignatureOfOfferId, String uid,
long currentDate, String messageVersion,
NodeAddress makerNodeAddress, @Nullable byte[] accountAgeWitnessSignatureOfOfferId,
NodeAddress takerNodeAddress, long currentDate,
NodeAddress arbitratorNodeAddress, NodeAddress makerNodeAddress,
@Nullable String reserveTxHash, NodeAddress takerNodeAddress,
@Nullable String reserveTxHex, NodeAddress arbitratorNodeAddress,
@Nullable String reserveTxKey, @Nullable String reserveTxHash,
@Nullable String payoutAddress, @Nullable String reserveTxHex,
@Nullable byte[] makerSignature) { @Nullable String reserveTxKey,
@Nullable String payoutAddress) {
super(messageVersion, offerId, uid); super(messageVersion, offerId, uid);
this.senderNodeAddress = senderNodeAddress; this.tradeProtocolVersion = tradeProtocolVersion;
this.pubKeyRing = pubKeyRing;
this.tradeAmount = tradeAmount; this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice; this.tradePrice = tradePrice;
this.accountId = accountId; this.makerAccountId = makerAccountId;
this.paymentAccountId = paymentAccountId; this.takerAccountId = takerAccountId;
this.makerPaymentAccountId = makerPaymentAccountId;
this.takerPaymentAccountId = takerPaymentAccountId;
this.takerPubKeyRing = takerPubKeyRing;
this.paymentMethodId = paymentMethodId; this.paymentMethodId = paymentMethodId;
this.accountAgeWitnessSignatureOfOfferId = accountAgeWitnessSignatureOfOfferId; this.accountAgeWitnessSignatureOfOfferId = accountAgeWitnessSignatureOfOfferId;
this.currentDate = currentDate; this.currentDate = currentDate;
@ -99,7 +99,6 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
this.reserveTxHex = reserveTxHex; this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey; this.reserveTxKey = reserveTxKey;
this.payoutAddress = payoutAddress; this.payoutAddress = payoutAddress;
this.makerSignature = makerSignature;
} }
@ -107,28 +106,30 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
// PROTO BUFFER // PROTO BUFFER
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Override
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.InitTradeRequest.Builder builder = protobuf.InitTradeRequest.newBuilder() protobuf.InitTradeRequest.Builder builder = protobuf.InitTradeRequest.newBuilder()
.setTradeProtocolVersion(TradeProtocolVersion.toProtoMessage(tradeProtocolVersion))
.setOfferId(offerId) .setOfferId(offerId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setTakerNodeAddress(takerNodeAddress.toProtoMessage()) .setTakerNodeAddress(takerNodeAddress.toProtoMessage())
.setMakerNodeAddress(makerNodeAddress.toProtoMessage()) .setMakerNodeAddress(makerNodeAddress.toProtoMessage())
.setTradeAmount(tradeAmount) .setTradeAmount(tradeAmount)
.setTradePrice(tradePrice) .setTradePrice(tradePrice)
.setPubKeyRing(pubKeyRing.toProtoMessage()) .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage())
.setPaymentAccountId(paymentAccountId) .setMakerPaymentAccountId(makerPaymentAccountId)
.setTakerPaymentAccountId(takerPaymentAccountId)
.setPaymentMethodId(paymentMethodId) .setPaymentMethodId(paymentMethodId)
.setAccountId(accountId) .setTakerAccountId(takerAccountId)
.setUid(uid); .setUid(uid);
Optional.ofNullable(makerAccountId).ifPresent(e -> builder.setMakerAccountId(makerAccountId));
Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())); Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()));
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash)); Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex)); Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey)); Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress)); Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress));
Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e)));
Optional.ofNullable(makerSignature).ifPresent(e -> builder.setMakerSignature(ByteString.copyFrom(e)));
builder.setCurrentDate(currentDate); builder.setCurrentDate(currentDate);
return getNetworkEnvelopeBuilder().setInitTradeRequest(builder).build(); return getNetworkEnvelopeBuilder().setInitTradeRequest(builder).build();
@ -137,14 +138,16 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
public static InitTradeRequest fromProto(protobuf.InitTradeRequest proto, public static InitTradeRequest fromProto(protobuf.InitTradeRequest proto,
CoreProtoResolver coreProtoResolver, CoreProtoResolver coreProtoResolver,
String messageVersion) { String messageVersion) {
return new InitTradeRequest(proto.getOfferId(), return new InitTradeRequest(TradeProtocolVersion.fromProto(proto.getTradeProtocolVersion()),
NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getOfferId(),
PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getTradeAmount(), proto.getTradeAmount(),
proto.getTradePrice(), proto.getTradePrice(),
proto.getAccountId(),
proto.getPaymentAccountId(),
proto.getPaymentMethodId(), proto.getPaymentMethodId(),
ProtoUtil.stringOrNullFromProto(proto.getMakerAccountId()),
proto.getTakerAccountId(),
proto.getMakerPaymentAccountId(),
proto.getTakerPaymentAccountId(),
PubKeyRing.fromProto(proto.getTakerPubKeyRing()),
proto.getUid(), proto.getUid(),
messageVersion, messageVersion,
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()), ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()),
@ -155,29 +158,31 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()), ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()), ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()));
ProtoUtil.byteArrayOrNullFromProto(proto.getMakerSignature()));
} }
@Override @Override
public String toString() { public String toString() {
return "InitTradeRequest{" + return "InitTradeRequest{" +
"\n senderNodeAddress=" + senderNodeAddress + "\n tradeProtocolVersion=" + tradeProtocolVersion +
",\n offerId=" + offerId + ",\n offerId=" + offerId +
",\n tradeAmount=" + tradeAmount + ",\n tradeAmount=" + tradeAmount +
",\n tradePrice=" + tradePrice + ",\n tradePrice=" + tradePrice +
",\n pubKeyRing=" + pubKeyRing +
",\n accountId='" + accountId + '\'' +
",\n paymentAccountId=" + paymentAccountId +
",\n paymentMethodId=" + paymentMethodId + ",\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 accountAgeWitnessSignatureOfOfferId=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfOfferId) +
",\n currentDate=" + currentDate + ",\n currentDate=" + currentDate +
",\n makerNodeAddress=" + makerNodeAddress +
",\n takerNodeAddress=" + takerNodeAddress +
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n reserveTxHash=" + reserveTxHash + ",\n reserveTxHash=" + reserveTxHash +
",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxHex=" + reserveTxHex +
",\n reserveTxKey=" + reserveTxKey + ",\n reserveTxKey=" + reserveTxKey +
",\n payoutAddress=" + payoutAddress + ",\n payoutAddress=" + payoutAddress +
",\n makerSignature=" + (makerSignature == null ? null : Utilities.byteArrayToInteger(makerSignature)) +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View file

@ -40,7 +40,7 @@ import haveno.core.trade.BuyerAsMakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.protocol.tasks.ApplyFilter; 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.core.trade.protocol.tasks.ProcessInitTradeRequest;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -71,7 +71,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
.setup(tasks( .setup(tasks(
ApplyFilter.class, ApplyFilter.class,
ProcessInitTradeRequest.class, ProcessInitTradeRequest.class,
MakerSendInitTradeRequest.class) MakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(); startTimeout();

View file

@ -40,9 +40,13 @@ import haveno.common.handlers.ErrorMessageHandler;
import haveno.core.trade.BuyerAsTakerTrade; import haveno.core.trade.BuyerAsTakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.handlers.TradeResultHandler; 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.ApplyFilter;
import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest;
import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds; import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds;
import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToMaker;
import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@ -64,31 +68,60 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
@Override @Override
public void onTakeOffer(TradeResultHandler tradeResultHandler, public void onTakeOffer(TradeResultHandler tradeResultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".onTakeOffer()"); System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
latchTrade(); latchTrade();
this.tradeResultHandler = tradeResultHandler; this.tradeResultHandler = tradeResultHandler;
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
expect(phase(Trade.Phase.INIT) expect(phase(Trade.Phase.INIT)
.with(TakerEvent.TAKE_OFFER) .with(TakerEvent.TAKE_OFFER)
.from(trade.getTradePeer().getNodeAddress())) .from(trade.getTradePeer().getNodeAddress()))
.setup(tasks( .setup(tasks(
ApplyFilter.class, ApplyFilter.class,
TakerReserveTradeFunds.class, TakerReserveTradeFunds.class,
TakerSendInitTradeRequestToArbitrator.class) TakerSendInitTradeRequestToMaker.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(); startTimeout();
unlatchTrade(); unlatchTrade();
}, },
errorMessage -> { errorMessage -> {
handleError(errorMessage); handleError(errorMessage);
})) }))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }
}, trade.getId()); }, 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());
} }
} }

View file

@ -41,7 +41,7 @@ import haveno.core.trade.SellerAsMakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.protocol.tasks.ApplyFilter; 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.core.trade.protocol.tasks.ProcessInitTradeRequest;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -76,7 +76,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
.setup(tasks( .setup(tasks(
ApplyFilter.class, ApplyFilter.class,
ProcessInitTradeRequest.class, ProcessInitTradeRequest.class,
MakerSendInitTradeRequest.class) MakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(); startTimeout();

View file

@ -40,12 +40,15 @@ import haveno.common.handlers.ErrorMessageHandler;
import haveno.core.trade.SellerAsTakerTrade; import haveno.core.trade.SellerAsTakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.handlers.TradeResultHandler; 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.ApplyFilter;
import haveno.core.trade.protocol.tasks.ProcessInitTradeRequest;
import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds; import haveno.core.trade.protocol.tasks.TakerReserveTradeFunds;
import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator; import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToArbitrator;
import haveno.core.trade.protocol.tasks.TakerSendInitTradeRequestToMaker;
import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
// TODO (woodser): remove unused request handling
@Slf4j @Slf4j
public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtocol { public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtocol {
@ -65,31 +68,60 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
@Override @Override
public void onTakeOffer(TradeResultHandler tradeResultHandler, public void onTakeOffer(TradeResultHandler tradeResultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
latchTrade(); latchTrade();
this.tradeResultHandler = tradeResultHandler; this.tradeResultHandler = tradeResultHandler;
this.errorMessageHandler = errorMessageHandler; this.errorMessageHandler = errorMessageHandler;
expect(phase(Trade.Phase.INIT) expect(phase(Trade.Phase.INIT)
.with(TakerEvent.TAKE_OFFER) .with(TakerEvent.TAKE_OFFER)
.from(trade.getTradePeer().getNodeAddress())) .from(trade.getTradePeer().getNodeAddress()))
.setup(tasks( .setup(tasks(
ApplyFilter.class, ApplyFilter.class,
TakerReserveTradeFunds.class, TakerReserveTradeFunds.class,
TakerSendInitTradeRequestToArbitrator.class) TakerSendInitTradeRequestToMaker.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(); startTimeout();
unlatchTrade(); unlatchTrade();
}, },
errorMessage -> { errorMessage -> {
handleError(errorMessage); handleError(errorMessage);
})) }))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }
}, trade.getId()); }, 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());
} }
} }

View file

@ -19,9 +19,12 @@ package haveno.core.trade.protocol;
import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ErrorMessageHandler;
import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.handlers.TradeResultHandler;
import haveno.core.trade.messages.InitTradeRequest;
import haveno.network.p2p.NodeAddress;
public interface TakerProtocol extends TraderProtocol { public interface TakerProtocol extends TraderProtocol {
void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler); void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler);
void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer);
enum TakerEvent implements FluentProtocol.Event { enum TakerEvent implements FluentProtocol.Event {
TAKE_OFFER TAKE_OFFER

View file

@ -48,10 +48,11 @@ public class ArbitratorProcessReserveTx extends TradeTask {
runInterceptHook(); runInterceptHook();
Offer offer = trade.getOffer(); Offer offer = trade.getOffer();
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); 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; 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 // process reserve tx with expected values
BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct()); BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct());
@ -73,7 +74,7 @@ public class ArbitratorProcessReserveTx extends TradeTask {
null); null);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); 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 // save reserve tx to model

View file

@ -24,6 +24,7 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitMultisigRequest; import haveno.core.trade.messages.InitMultisigRequest;
import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.protocol.TradePeer;
import haveno.network.p2p.SendDirectMessageListener; import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
@ -50,20 +51,23 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage();
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
// handle request from taker // handle request from maker
if (request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress())) { if (sender == trade.getMaker()) {
// create request to initialize trade with maker // create request to taker
InitTradeRequest makerRequest = new InitTradeRequest( InitTradeRequest takerRequest = new InitTradeRequest(
request.getTradeProtocolVersion(),
processModel.getOfferId(), processModel.getOfferId(),
request.getSenderNodeAddress(),
request.getPubKeyRing(),
trade.getAmount().longValueExact(), trade.getAmount().longValueExact(),
trade.getPrice().getValue(), trade.getPrice().getValue(),
request.getAccountId(),
request.getPaymentAccountId(),
request.getPaymentMethodId(), request.getPaymentMethodId(),
request.getMakerAccountId(),
request.getTakerAccountId(),
request.getMakerPaymentAccountId(),
request.getTakerPaymentAccountId(),
request.getTakerPubKeyRing(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
request.getAccountAgeWitnessSignatureOfOfferId(), request.getAccountAgeWitnessSignatureOfOfferId(),
@ -72,35 +76,34 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
trade.getTaker().getNodeAddress(), trade.getTaker().getNodeAddress(),
trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getNodeAddress(),
null, null,
null, // do not include taker's reserve tx
null, null,
null, null,
null); null);
// send request to maker // send request to taker
log.info("Send {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress()); log.info("Send {} with offerId {} and uid {} to taker {}", takerRequest.getClass().getSimpleName(), takerRequest.getOfferId(), takerRequest.getUid(), trade.getTaker().getNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage( 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.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.getMaker().getPubKeyRing(), trade.getTaker().getPubKeyRing(),
makerRequest, takerRequest,
new SendDirectMessageListener() { new SendDirectMessageListener() {
@Override @Override
public void onArrived() { 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(); complete();
} }
@Override @Override
public void onFault(String errorMessage) { public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", makerRequest.getClass().getSimpleName(), makerRequest.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage); log.error("Sending {} failed: uid={}; peer={}; error={}", takerRequest.getClass().getSimpleName(), takerRequest.getUid(), trade.getTaker().getNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + makerRequest + "\nerrorMessage=" + errorMessage); appendToErrorMessage("Sending message failed: message=" + takerRequest + "\nerrorMessage=" + errorMessage);
failed(); failed();
} }
} }
); );
} }
// handle request from maker // handle request from taker
else if (request.getSenderNodeAddress().equals(trade.getMaker().getNodeAddress())) { else if (sender == trade.getTaker()) {
sendInitMultisigRequests(); sendInitMultisigRequests();
complete(); // TODO: wait for InitMultisigRequest arrivals? complete(); // TODO: wait for InitMultisigRequest arrivals?
} else { } else {
@ -113,10 +116,9 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
private void sendInitMultisigRequests() { private void sendInitMultisigRequests() {
// ensure arbitrator has maker's reserve tx // ensure arbitrator has reserve txs
if (processModel.getMaker().getReserveTxHash() == null) { if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade");
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 // create wallet for multisig
MoneroWallet multisigWallet = trade.createWallet(); MoneroWallet multisigWallet = trade.createWallet();

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<NodeAddress>(), () -> {
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<NodeAddress> 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
);
}
}

View file

@ -21,11 +21,13 @@ import com.google.common.base.Charsets;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.exceptions.TradePriceOutOfToleranceException; import haveno.core.exceptions.TradePriceOutOfToleranceException;
import haveno.core.offer.Offer; import haveno.core.offer.Offer;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade; import haveno.core.trade.MakerTrade;
import haveno.core.trade.TakerTrade;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitTradeRequest; import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.messages.TradeProtocolVersion;
import haveno.core.trade.protocol.TradePeer; import haveno.core.trade.protocol.TradePeer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -50,62 +52,31 @@ public class ProcessInitTradeRequest extends TradeTask {
runInterceptHook(); runInterceptHook();
Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null");
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage(); InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage();
// validate
checkNotNull(request); checkNotNull(request);
checkTradeId(processModel.getOfferId(), request); checkTradeId(processModel.getOfferId(), request);
checkArgument(request.getTradeAmount() > 0);
// validate inputs
if (trade.getAmount().compareTo(trade.getOffer().getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount"); 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"); if (trade.getAmount().compareTo(trade.getOffer().getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount");
if (!request.getTakerNodeAddress().equals(trade.getTaker().getNodeAddress())) throw new RuntimeException("Trade's taker node address does not match request");
// handle request as arbitrator if (!request.getMakerNodeAddress().equals(trade.getMaker().getNodeAddress())) throw new RuntimeException("Trade's maker node address does not match request");
TradePeer multisigParticipant; if (!request.getOfferId().equals(offer.getId())) throw new RuntimeException("Offer id does not match request's offer id");
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");
}
}
// handle request as maker // handle request as maker
else if (trade instanceof MakerTrade) { TradePeer sender;
multisigParticipant = processModel.getTaker(); if (trade instanceof MakerTrade) {
trade.getTaker().setNodeAddress(request.getSenderNodeAddress()); // arbitrator sends maker InitTradeRequest with taker's node address and pub key ring sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
trade.getTaker().setPubKeyRing(request.getPubKeyRing()); 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 // check trade price
try { try {
long tradePrice = request.getTradePrice(); long tradePrice = request.getTradePrice();
offer.verifyTakersTradePrice(tradePrice); offer.verifyTradePrice(tradePrice);
trade.setPrice(tradePrice); trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) { } catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage()); 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 // handle invalid trade type
else { else {
throw new RuntimeException("Invalid trade type to process init trade request: " + trade.getClass().getName()); throw new RuntimeException("Invalid trade type to process init trade request: " + trade.getClass().getName());
} }
// set trading peer info // set trading peer info
if (multisigParticipant.getPaymentAccountId() == null) multisigParticipant.setPaymentAccountId(request.getPaymentAccountId()); if (trade.getMaker().getAccountId() == null) trade.getMaker().setAccountId(request.getMakerAccountId());
else if (multisigParticipant.getPaymentAccountId() != request.getPaymentAccountId()) throw new RuntimeException("Payment account id is different from previous"); else if (!trade.getMaker().getAccountId().equals(request.getMakerAccountId())) throw new RuntimeException("Maker account id is different from previous");
multisigParticipant.setPubKeyRing(checkNotNull(request.getPubKeyRing())); if (trade.getTaker().getAccountId() == null) trade.getTaker().setAccountId(request.getTakerAccountId());
multisigParticipant.setAccountId(nonEmptyStringOf(request.getAccountId())); else if (!trade.getTaker().getAccountId().equals(request.getTakerAccountId())) throw new RuntimeException("Taker account id is different from previous");
multisigParticipant.setPaymentMethodId(nonEmptyStringOf(request.getPaymentMethodId())); if (trade.getMaker().getPaymentAccountId() == null) trade.getMaker().setPaymentAccountId(request.getMakerPaymentAccountId());
multisigParticipant.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); else if (!trade.getMaker().getPaymentAccountId().equals(request.getMakerPaymentAccountId())) throw new RuntimeException("Maker payment account id is different from previous");
multisigParticipant.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId()); if (trade.getTaker().getPaymentAccountId() == null) trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId());
multisigParticipant.setCurrentDate(request.getCurrentDate()); 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 // check peer's current date
processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(multisigParticipant.getCurrentDate())); processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(sender.getCurrentDate()));
// check trade amount
checkArgument(request.getTradeAmount() > 0);
checkArgument(request.getTradeAmount() == trade.getAmount().longValueExact(), "Trade amount does not match request's trade amount");
// persist trade // persist trade
trade.addInitProgressStep(); trade.addInitProgressStep();

View file

@ -95,11 +95,14 @@ public class TakerReserveTradeFunds extends TradeTask {
trade.startProtocolTimeout(); trade.startProtocolTimeout();
// update trade state // update trade state
trade.getTaker().setReserveTxHash(reserveTx.getHash());
trade.getTaker().setReserveTxHex(reserveTx.getFullHex());
trade.getTaker().setReserveTxKey(reserveTx.getKey());
trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx)); trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
} }
// save process state // save process state
processModel.setReserveTx(reserveTx); processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used?
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
trade.addInitProgressStep(); trade.addInitProgressStep();
complete(); complete();

View file

@ -18,24 +18,22 @@
package haveno.core.trade.protocol.tasks; package haveno.core.trade.protocol.tasks;
import haveno.common.app.Version; import haveno.common.app.Version;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.handlers.ResultHandler;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.availability.DisputeAgentSelection; import haveno.core.offer.Offer;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitTradeRequest; 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 haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.HashSet; import java.util.UUID;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
import static haveno.core.util.Validator.checkTradeId;
@Slf4j @Slf4j
public class TakerSendInitTradeRequestToArbitrator extends TradeTask { public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
@SuppressWarnings({"unused"}) @SuppressWarnings({"unused"})
public TakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) { public TakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
@ -46,106 +44,57 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
// get least used arbitrator // verify trade state
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager()); InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker
if (leastUsedArbitrator == null) { checkNotNull(sourceRequest);
failed("Could not get least used arbitrator to send " + InitTradeRequest.class.getSimpleName() + " for offer " + trade.getId()); checkTradeId(processModel.getOfferId(), sourceRequest);
return; 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 // create request to arbitrator
sendInitTradeRequests(leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), () -> { Offer offer = processModel.getOffer();
trade.addInitProgressStep(); InitTradeRequest arbitratorRequest = new InitTradeRequest(
complete(); TradeProtocolVersion.MULTISIG_2_3, // TODO: use processModel.getTradeProtocolVersion(), select one of maker's supported versions
}, (errorMessage) -> { offer.getId(),
log.warn("Cannot initialize trade with arbitrators: " + errorMessage); trade.getAmount().longValueExact(),
failed(errorMessage); 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) { } catch (Throwable t) {
failed(t); failed(t);
} }
} }
private void sendInitTradeRequests(NodeAddress arbitratorNodeAddress, Set<NodeAddress> 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
);
}
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View file

@ -733,7 +733,7 @@ public class XmrWalletService {
* The transaction is submitted to the pool then flushed without relaying. * The transaction is submitted to the pool then flushed without relaying.
* *
* @param offerId id of offer to verify trade tx * @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 feeAddress fee address
* @param sendAmount amount sent to transfer address * @param sendAmount amount sent to transfer address
* @param sendAddress transfer address * @param sendAddress transfer address
@ -743,7 +743,7 @@ public class XmrWalletService {
* @param keyImages expected key images of inputs, ignored if null * @param keyImages expected key images of inputs, ignored if null
* @return the verified tx * @return the verified tx
*/ */
public MoneroTx verifyTradeTx(String offerId, BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List<String> keyImages) { public MoneroTx verifyTradeTx(String offerId, BigInteger tradeFeeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List<String> keyImages) {
if (txHash == null) throw new IllegalArgumentException("Cannot verify trade tx with null id"); if (txHash == null) throw new IllegalArgumentException("Cannot verify trade tx with null id");
MoneroDaemonRpc daemon = getDaemon(); MoneroDaemonRpc daemon = getDaemon();
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
@ -780,11 +780,11 @@ public class XmrWalletService {
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff);
// verify proof to fee address // verify proof to fee address
BigInteger actualFee = BigInteger.ZERO; BigInteger actualTradeFee = BigInteger.ZERO;
if (feeAmount.compareTo(BigInteger.ZERO) > 0) { if (tradeFeeAmount.compareTo(BigInteger.ZERO) > 0) {
MoneroCheckTx feeCheck = wallet.checkTxKey(txHash, txKey, feeAddress); MoneroCheckTx tradeFeeCheck = wallet.checkTxKey(txHash, txKey, feeAddress);
if (!feeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address"); if (!tradeFeeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address");
actualFee = feeCheck.getReceivedAmount(); actualTradeFee = tradeFeeCheck.getReceivedAmount();
} }
// verify proof to transfer address // verify proof to transfer address
@ -792,15 +792,15 @@ public class XmrWalletService {
if (!transferCheck.isGood()) throw new RuntimeException("Invalid proof to transfer address"); if (!transferCheck.isGood()) throw new RuntimeException("Invalid proof to transfer address");
BigInteger actualSendAmount = transferCheck.getReceivedAmount(); BigInteger actualSendAmount = transferCheck.getReceivedAmount();
// verify fee amount // verify trade fee amount
if (!actualFee.equals(feeAmount)) throw new RuntimeException("Invalid fee amount, expected " + feeAmount + " but was " + actualFee); if (!actualTradeFee.equals(tradeFeeAmount)) throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee);
// verify send amount // verify send amount
BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee()); 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()); if (!actualSendAmount.equals(expectedSendAmount)) throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee());
return tx; return tx;
} catch (Exception e) { } 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; throw e;
} finally { } finally {
try { try {

View file

@ -679,7 +679,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
}); });
model.getTopErrorMsg().addListener((ov, oldValue, newValue) -> { model.getTopErrorMsg().addListener((ov, oldValue, newValue) -> {
log.warn("top level warning has been set! " + newValue); log.warn("Top level warning: " + newValue);
if (newValue != null) { if (newValue != null) {
new Popup().warning(newValue).show(); new Popup().warning(newValue).show();
} }

View file

@ -225,26 +225,31 @@ message PrefixedSealedAndSignedMessage {
string uid = 4; string uid = 4;
} }
enum TradeProtocolVersion {
MULTISIG_2_3 = 0;
}
message InitTradeRequest { message InitTradeRequest {
string offer_id = 1; TradeProtocolVersion trade_protocol_version = 1;
NodeAddress sender_node_address = 2; string offer_id = 2;
PubKeyRing pub_key_ring = 3; int64 trade_amount = 3;
int64 trade_amount = 4; int64 trade_price = 4;
int64 trade_price = 5; string payment_method_id = 5;
string account_id = 6; string maker_account_id = 6;
string payment_account_id = 7; string taker_account_id = 7;
string payment_method_id = 8; string maker_payment_account_id = 8;
string uid = 9; string taker_payment_account_id = 9;
bytes account_age_witness_signature_of_offer_id = 10; PubKeyRing taker_pub_key_ring = 10;
int64 current_date = 11; string uid = 11;
NodeAddress maker_node_address = 12; bytes account_age_witness_signature_of_offer_id = 12;
NodeAddress taker_node_address = 13; int64 current_date = 13;
NodeAddress arbitrator_node_address = 14; NodeAddress maker_node_address = 14;
string reserve_tx_hash = 15; NodeAddress taker_node_address = 15;
string reserve_tx_hex = 16; NodeAddress arbitrator_node_address = 16;
string reserve_tx_key = 17; string reserve_tx_hash = 17;
string payout_address = 18; string reserve_tx_hex = 18;
bytes maker_signature = 19; string reserve_tx_key = 19;
string payout_address = 20;
} }
message InitMultisigRequest { message InitMultisigRequest {