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();
}
public void verifyTakersTradePrice(long takersTradePrice) throws TradePriceOutOfToleranceException,
public void verifyTradePrice(long price) throws TradePriceOutOfToleranceException,
MarketPriceNotAvailableException, IllegalArgumentException {
if (!isUseMarketBasedPrice()) {
checkArgument(takersTradePrice == getFixedPrice(),
checkArgument(price == getFixedPrice(),
"Takers price does not match offer price. " +
"Takers price=" + takersTradePrice + "; offer price=" + getFixedPrice());
"Takers price=" + price + "; offer price=" + getFixedPrice());
return;
}
Price tradePrice = Price.valueOf(getCurrencyCode(), takersTradePrice);
Price tradePrice = Price.valueOf(getCurrencyCode(), price);
Price offerPrice = getPrice();
if (offerPrice == null)
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available.");
checkArgument(takersTradePrice > 0, "takersTradePrice must be positive");
checkArgument(price > 0, "takersTradePrice must be positive");
double relation = (double) takersTradePrice / (double) offerPrice.getValue();
double relation = (double) price / (double) offerPrice.getValue();
// We allow max. 2 % difference between own offerPayload price calculation and takers calculation.
// Market price might be different at maker's and takers side so we need a bit of tolerance.
// The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations
@ -234,7 +234,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
double deviation = Math.abs(1 - relation);
log.info("Price at take-offer time: id={}, currency={}, takersPrice={}, makersPrice={}, deviation={}",
getShortId(), getCurrencyCode(), takersTradePrice, offerPrice.getValue(),
getShortId(), getCurrencyCode(), price, offerPrice.getValue(),
deviation * 100 + "%");
if (deviation > PRICE_TOLERANCE) {
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +

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

View File

@ -23,7 +23,6 @@ import haveno.core.offer.AvailabilityResult;
import haveno.core.offer.Offer;
import haveno.core.offer.availability.OfferAvailabilityModel;
import haveno.core.offer.messages.OfferAvailabilityResponse;
import haveno.core.trade.HavenoUtils;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@ -52,13 +51,6 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
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);
model.setMakerSignature(offerAvailabilityResponse.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.trade.HavenoUtils;
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;
@ -53,8 +54,9 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
User user = model.getUser();
P2PService p2PService = model.getP2PService();
XmrWalletService walletService = model.getXmrWalletService();
String paymentAccountId = model.getPaymentAccountId();
String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
String makerPaymentAccountId = offer.getOfferPayload().getMakerPaymentAccountId();
String takerPaymentAccountId = model.getPaymentAccountId();
String paymentMethodId = user.getPaymentAccount(takerPaymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
// taker signs offer using offer id as nonce to avoid challenge protocol
@ -66,14 +68,16 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
// send InitTradeRequest to maker to sign
InitTradeRequest tradeRequest = new InitTradeRequest(
TradeProtocolVersion.MULTISIG_2_3, // TODO: replace with first of their accepted protocols
offer.getId(),
P2PService.getMyNodeAddress(),
p2PService.getKeyRing().getPubKeyRing(),
model.getTradeAmount().longValueExact(),
price.getValue(),
user.getAccountId(),
paymentAccountId,
paymentMethodId,
null,
user.getAccountId(),
makerPaymentAccountId,
takerPaymentAccountId,
p2PService.getKeyRing().getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
sig,
@ -84,8 +88,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
null, // reserve tx not sent from taker to maker
null,
null,
payoutAddress,
null);
payoutAddress);
// save trade request to later send to arbitrator
model.setTradeRequest(tradeRequest);

View File

@ -32,7 +32,6 @@ import haveno.core.app.HavenoSetup;
import haveno.core.offer.OfferPayload;
import haveno.core.support.dispute.arbitration.ArbitrationManager;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.util.JsonUtil;
@ -326,52 +325,6 @@ public class HavenoUtils {
return isSignatureValid(arbitrator.getPubKeyRing(), offer.getSignatureHash(), offer.getArbitratorSignature());
}
/**
* Check if the maker signature for a trade request is valid.
*
* @param request is the trade request to check
* @return true if the maker's signature is valid for the trade request
*/
public static boolean isMakerSignatureValid(InitTradeRequest request, byte[] signature, PubKeyRing makerPubKeyRing) {
// re-create trade request with signed fields
InitTradeRequest signedRequest = new InitTradeRequest(
request.getOfferId(),
request.getSenderNodeAddress(),
request.getPubKeyRing(),
request.getTradeAmount(),
request.getTradePrice(),
request.getAccountId(),
request.getPaymentAccountId(),
request.getPaymentMethodId(),
request.getUid(),
request.getMessageVersion(),
request.getAccountAgeWitnessSignatureOfOfferId(),
request.getCurrentDate(),
request.getMakerNodeAddress(),
request.getTakerNodeAddress(),
null,
null,
null,
null,
request.getPayoutAddress(),
null
);
// get trade request as string
String tradeRequestAsJson = JsonUtil.objectToJson(signedRequest);
// verify maker signature
boolean isSignatureValid = isSignatureValid(makerPubKeyRing, tradeRequestAsJson, signature);
if (!isSignatureValid) {
log.warn("Invalid maker signature for trade request: " + request.getOfferId() + " from " + request.getSenderNodeAddress().getAddressForDisplay());
log.warn("Trade request as json: " + tradeRequestAsJson);
log.warn("Maker pub key ring: " + (makerPubKeyRing == null ? null : "..."));
log.warn("Maker signature: " + (signature == null ? null : Utilities.bytesAsHexString(signature)));
}
return isSignatureValid;
}
/**
* Verify the buyer signature for a PaymentSentMessage.
*

View File

@ -362,7 +362,7 @@ public abstract class Trade implements Tradable, Model {
private long takeOfferDate;
// Initialization
private static final int TOTAL_INIT_STEPS = 23; // total estimated steps
private static final int TOTAL_INIT_STEPS = 24; // total estimated steps
private int initStep = 0;
@Getter
private double initProgress = 0;
@ -1552,6 +1552,7 @@ public abstract class Trade implements Tradable, Model {
public void addInitProgressStep() {
startProtocolTimeout();
initProgress = Math.min(1.0, (double) ++initStep / TOTAL_INIT_STEPS);
//if (this instanceof TakerTrade) log.warn("Init step count: " + initStep); // log init step count for taker trades in order to update total steps
UserThread.execute(() -> initProgressProperty.set(initProgress));
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,10 +48,11 @@ public class ArbitratorProcessReserveTx extends TradeTask {
runInterceptHook();
Offer offer = trade.getOffer();
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage();
boolean isFromMaker = request.getSenderNodeAddress().equals(trade.getMaker().getNodeAddress());
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
boolean isFromMaker = sender == trade.getMaker();
boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL;
// TODO (woodser): if signer online, should never be called by maker
// TODO (woodser): if signer online, should never be called by maker?
// process reserve tx with expected values
BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct());
@ -73,7 +74,7 @@ public class ArbitratorProcessReserveTx extends TradeTask {
null);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// save reserve tx to model

View File

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

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.core.exceptions.TradePriceOutOfToleranceException;
import haveno.core.offer.Offer;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.TakerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.messages.TradeProtocolVersion;
import haveno.core.trade.protocol.TradePeer;
import lombok.extern.slf4j.Slf4j;
@ -50,62 +52,31 @@ public class ProcessInitTradeRequest extends TradeTask {
runInterceptHook();
Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null");
InitTradeRequest request = (InitTradeRequest) processModel.getTradeMessage();
// validate
checkNotNull(request);
checkTradeId(processModel.getOfferId(), request);
// validate inputs
checkArgument(request.getTradeAmount() > 0);
if (trade.getAmount().compareTo(trade.getOffer().getAmount()) > 0) throw new RuntimeException("Trade amount exceeds offer amount");
if (trade.getAmount().compareTo(trade.getOffer().getMinAmount()) < 0) throw new RuntimeException("Trade amount is less than minimum offer amount");
// handle request as arbitrator
TradePeer multisigParticipant;
if (trade instanceof ArbitratorTrade) {
trade.getMaker().setPubKeyRing((trade.getOffer().getPubKeyRing()));
trade.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO (woodser): why duplicating field in process model
// handle request from taker
if (request.getSenderNodeAddress().equals(request.getTakerNodeAddress())) {
multisigParticipant = processModel.getTaker();
if (!trade.getTaker().getNodeAddress().equals(request.getTakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree");
if (trade.getTaker().getPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest");
trade.getTaker().setPubKeyRing(request.getPubKeyRing());
if (!HavenoUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature
// check trade price
try {
long tradePrice = request.getTradePrice();
offer.verifyTakersTradePrice(tradePrice);
trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage());
} catch (Throwable e2) {
failed(e2);
}
}
// handle request from maker
else if (request.getSenderNodeAddress().equals(request.getMakerNodeAddress())) {
multisigParticipant = processModel.getMaker();
if (!trade.getMaker().getNodeAddress().equals(request.getMakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); // TODO (woodser): test when maker and taker do not agree, use proper handling, uninitialize trade for other takers
if (trade.getMaker().getPubKeyRing() == null) trade.getMaker().setPubKeyRing(request.getPubKeyRing());
else if (!trade.getMaker().getPubKeyRing().equals(request.getPubKeyRing())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); // TODO (woodser): proper handling
trade.getMaker().setPubKeyRing(request.getPubKeyRing());
if (trade.getPrice().getValue() != request.getTradePrice()) throw new RuntimeException("Maker and taker price do not agree");
} else {
throw new RuntimeException("Sender is not trade's maker or taker");
}
}
if (!request.getTakerNodeAddress().equals(trade.getTaker().getNodeAddress())) throw new RuntimeException("Trade's taker node address does not match request");
if (!request.getMakerNodeAddress().equals(trade.getMaker().getNodeAddress())) throw new RuntimeException("Trade's maker node address does not match request");
if (!request.getOfferId().equals(offer.getId())) throw new RuntimeException("Offer id does not match request's offer id");
// handle request as maker
else if (trade instanceof MakerTrade) {
multisigParticipant = processModel.getTaker();
trade.getTaker().setNodeAddress(request.getSenderNodeAddress()); // arbitrator sends maker InitTradeRequest with taker's node address and pub key ring
trade.getTaker().setPubKeyRing(request.getPubKeyRing());
TradePeer sender;
if (trade instanceof MakerTrade) {
sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender != trade.getTaker()) throw new RuntimeException("InitTradeRequest to maker is expected from taker");
trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing());
// check protocol version
if (request.getTradeProtocolVersion() != TradeProtocolVersion.MULTISIG_2_3) throw new RuntimeException("Trade protocol version is not supported"); // TODO: check if contained in supported versions
// check trade price
try {
long tradePrice = request.getTradePrice();
offer.verifyTakersTradePrice(tradePrice);
offer.verifyTradePrice(tradePrice);
trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage());
@ -114,27 +85,78 @@ public class ProcessInitTradeRequest extends TradeTask {
}
}
// handle request as arbitrator
else if (trade instanceof ArbitratorTrade) {
trade.getMaker().setPubKeyRing((trade.getOffer().getPubKeyRing())); // TODO: why initializing this here fields here and
trade.getArbitrator().setPubKeyRing(processModel.getPubKeyRing()); // TODO: why duplicating field in process model?
if (!trade.getArbitrator().getNodeAddress().equals(request.getArbitratorNodeAddress())) throw new RuntimeException("Trade's arbitrator node address does not match request");
// check protocol version
if (request.getTradeProtocolVersion() != TradeProtocolVersion.MULTISIG_2_3) throw new RuntimeException("Trade protocol version is not supported"); // TODO: check consistent from maker and taker when multiple protocols supported
// handle request from maker
sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender == trade.getMaker()) {
trade.getTaker().setPubKeyRing(request.getTakerPubKeyRing());
// check trade price
try {
long tradePrice = request.getTradePrice();
offer.verifyTradePrice(tradePrice);
trade.setPrice(tradePrice);
} catch (TradePriceOutOfToleranceException e) {
failed(e.getMessage());
} catch (Throwable e2) {
failed(e2);
}
}
// handle request from taker
else if (sender == trade.getTaker()) {
if (!trade.getTaker().getPubKeyRing().equals(request.getTakerPubKeyRing())) throw new RuntimeException("Taker's pub key ring does not match request's pub key ring");
if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount");
if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price");
}
// handle invalid sender
else {
throw new RuntimeException("Sender is not trade's maker or taker");
}
}
// handle request as taker
else if (trade instanceof TakerTrade) {
if (request.getTradeAmount() != trade.getAmount().longValueExact()) throw new RuntimeException("Trade amount does not match request's trade amount");
if (request.getTradePrice() != trade.getPrice().getValue()) throw new RuntimeException("Trade price does not match request's trade price");
Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(request.getArbitratorNodeAddress());
if (arbitrator == null) throw new RuntimeException("Arbitrator is not accepted by taker");
trade.getArbitrator().setNodeAddress(request.getArbitratorNodeAddress());
trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender != trade.getArbitrator()) throw new RuntimeException("InitTradeRequest to taker is expected from arbitrator");
}
// handle invalid trade type
else {
throw new RuntimeException("Invalid trade type to process init trade request: " + trade.getClass().getName());
}
// set trading peer info
if (multisigParticipant.getPaymentAccountId() == null) multisigParticipant.setPaymentAccountId(request.getPaymentAccountId());
else if (multisigParticipant.getPaymentAccountId() != request.getPaymentAccountId()) throw new RuntimeException("Payment account id is different from previous");
multisigParticipant.setPubKeyRing(checkNotNull(request.getPubKeyRing()));
multisigParticipant.setAccountId(nonEmptyStringOf(request.getAccountId()));
multisigParticipant.setPaymentMethodId(nonEmptyStringOf(request.getPaymentMethodId()));
multisigParticipant.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8));
multisigParticipant.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId());
multisigParticipant.setCurrentDate(request.getCurrentDate());
if (trade.getMaker().getAccountId() == null) trade.getMaker().setAccountId(request.getMakerAccountId());
else if (!trade.getMaker().getAccountId().equals(request.getMakerAccountId())) throw new RuntimeException("Maker account id is different from previous");
if (trade.getTaker().getAccountId() == null) trade.getTaker().setAccountId(request.getTakerAccountId());
else if (!trade.getTaker().getAccountId().equals(request.getTakerAccountId())) throw new RuntimeException("Taker account id is different from previous");
if (trade.getMaker().getPaymentAccountId() == null) trade.getMaker().setPaymentAccountId(request.getMakerPaymentAccountId());
else if (!trade.getMaker().getPaymentAccountId().equals(request.getMakerPaymentAccountId())) throw new RuntimeException("Maker payment account id is different from previous");
if (trade.getTaker().getPaymentAccountId() == null) trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId());
else if (!trade.getTaker().getPaymentAccountId().equals(request.getTakerPaymentAccountId())) throw new RuntimeException("Taker payment account id is different from previous");
sender.setPaymentMethodId(nonEmptyStringOf(request.getPaymentMethodId())); // TODO: move to process model?
sender.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8));
sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfOfferId());
sender.setCurrentDate(request.getCurrentDate());
// check peer's current date
processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(multisigParticipant.getCurrentDate()));
// check trade amount
checkArgument(request.getTradeAmount() > 0);
checkArgument(request.getTradeAmount() == trade.getAmount().longValueExact(), "Trade amount does not match request's trade amount");
processModel.getAccountAgeWitnessService().verifyPeersCurrentDate(new Date(sender.getCurrentDate()));
// persist trade
trade.addInitProgressStep();

View File

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

View File

@ -18,24 +18,22 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.app.Version;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.handlers.ResultHandler;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.availability.DisputeAgentSelection;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.HavenoUtils;
import haveno.core.offer.Offer;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.InitTradeRequest;
import haveno.network.p2p.NodeAddress;
import haveno.core.trade.messages.TradeProtocolVersion;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import static com.google.common.base.Preconditions.checkNotNull;
import static haveno.core.util.Validator.checkTradeId;
@Slf4j
public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
@SuppressWarnings({"unused"})
public TakerSendInitTradeRequestToArbitrator(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
@ -46,106 +44,57 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
try {
runInterceptHook();
// get least used arbitrator
Arbitrator leastUsedArbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager());
if (leastUsedArbitrator == null) {
failed("Could not get least used arbitrator to send " + InitTradeRequest.class.getSimpleName() + " for offer " + trade.getId());
return;
}
// verify trade state
InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker
checkNotNull(sourceRequest);
checkTradeId(processModel.getOfferId(), sourceRequest);
if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
// send request to least used arbitrators until success
sendInitTradeRequests(leastUsedArbitrator.getNodeAddress(), new HashSet<NodeAddress>(), () -> {
trade.addInitProgressStep();
complete();
}, (errorMessage) -> {
log.warn("Cannot initialize trade with arbitrators: " + errorMessage);
failed(errorMessage);
});
// create request to arbitrator
Offer offer = processModel.getOffer();
InitTradeRequest arbitratorRequest = new InitTradeRequest(
TradeProtocolVersion.MULTISIG_2_3, // TODO: use processModel.getTradeProtocolVersion(), select one of maker's supported versions
offer.getId(),
trade.getAmount().longValueExact(),
trade.getPrice().getValue(),
offer.getOfferPayload().getPaymentMethodId(),
trade.getMaker().getAccountId(),
trade.getTaker().getAccountId(),
trade.getMaker().getPaymentAccountId(),
trade.getTaker().getPaymentAccountId(),
trade.getTaker().getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
null,
sourceRequest.getCurrentDate(),
trade.getMaker().getNodeAddress(),
trade.getTaker().getNodeAddress(),
trade.getArbitrator().getNodeAddress(),
trade.getSelf().getReserveTxHash(),
trade.getSelf().getReserveTxHex(),
trade.getSelf().getReserveTxKey(),
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString());
// send request to arbitrator
log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getArbitrator().getNodeAddress(),
trade.getArbitrator().getPubKeyRing(),
arbitratorRequest,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
complete();
}
@Override
public void onFault(String errorMessage) {
log.warn("Failed to send {} to arbitrator, error={}.", InitTradeRequest.class.getSimpleName(), errorMessage);
failed();
}
});
} catch (Throwable t) {
failed(t);
failed(t);
}
}
private void sendInitTradeRequests(NodeAddress arbitratorNodeAddress, Set<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.
*
* @param offerId id of offer to verify trade tx
* @param feeAmount amount sent to fee address
* @param tradeFeeAmount amount sent to fee address
* @param feeAddress fee address
* @param sendAmount amount sent to transfer address
* @param sendAddress transfer address
@ -743,7 +743,7 @@ public class XmrWalletService {
* @param keyImages expected key images of inputs, ignored if null
* @return the verified tx
*/
public MoneroTx verifyTradeTx(String offerId, BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, String txHash, String txHex, String txKey, List<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");
MoneroDaemonRpc daemon = getDaemon();
MoneroWallet wallet = getWallet();
@ -780,11 +780,11 @@ public class XmrWalletService {
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff);
// verify proof to fee address
BigInteger actualFee = BigInteger.ZERO;
if (feeAmount.compareTo(BigInteger.ZERO) > 0) {
MoneroCheckTx feeCheck = wallet.checkTxKey(txHash, txKey, feeAddress);
if (!feeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address");
actualFee = feeCheck.getReceivedAmount();
BigInteger actualTradeFee = BigInteger.ZERO;
if (tradeFeeAmount.compareTo(BigInteger.ZERO) > 0) {
MoneroCheckTx tradeFeeCheck = wallet.checkTxKey(txHash, txKey, feeAddress);
if (!tradeFeeCheck.isGood()) throw new RuntimeException("Invalid proof to trade fee address");
actualTradeFee = tradeFeeCheck.getReceivedAmount();
}
// verify proof to transfer address
@ -792,15 +792,15 @@ public class XmrWalletService {
if (!transferCheck.isGood()) throw new RuntimeException("Invalid proof to transfer address");
BigInteger actualSendAmount = transferCheck.getReceivedAmount();
// verify fee amount
if (!actualFee.equals(feeAmount)) throw new RuntimeException("Invalid fee amount, expected " + feeAmount + " but was " + actualFee);
// verify trade fee amount
if (!actualTradeFee.equals(tradeFeeAmount)) throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee);
// verify send amount
BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee());
if (!actualSendAmount.equals(expectedSendAmount)) throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee());
return tx;
} catch (Exception e) {
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage());
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=\n" + tx) + ": " + e.getMessage());
throw e;
} finally {
try {

View File

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

View File

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