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