mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
maker selects arbitrator (breaking change)
This commit is contained in:
parent
6df5296dcd
commit
1150d929af
25 changed files with 920 additions and 718 deletions
|
@ -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" +
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue