support buying xmr without deposit or fee using passphrase

This commit is contained in:
woodser 2024-12-16 07:04:53 -05:00
parent ece3b0fec0
commit 775fbc41c2
115 changed files with 3845 additions and 838 deletions

View file

@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection;
import haveno.core.offer.OfferRestrictions;
import haveno.core.payment.ChargeBackRisk;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.TradeLimits;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.support.dispute.Dispute;
@ -498,10 +499,15 @@ public class AccountAgeWitnessService {
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
}
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) {
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) {
if (paymentAccount == null)
return 0;
if (buyerAsTakerWithoutDeposit) {
TradeLimits tradeLimits = new TradeLimits();
return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact();
}
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
if (hasTradeLimitException(accountAgeWitness)) {

View file

@ -419,10 +419,12 @@ public class CoreApi {
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double buyerSecurityDeposit,
double securityDepositPct,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreOffersService.postOffer(currencyCode,
@ -432,10 +434,12 @@ public class CoreApi {
marketPriceMargin,
amountAsLong,
minAmountAsLong,
buyerSecurityDeposit,
securityDepositPct,
triggerPriceAsString,
reserveExactAmount,
paymentAccountId,
isPrivateOffer,
buyerAsTakerWithoutDeposit,
resultHandler,
errorMessageHandler);
}
@ -448,8 +452,10 @@ public class CoreApi {
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
double buyerSecurityDeposit,
PaymentAccount paymentAccount) {
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit) {
return coreOffersService.editOffer(offerId,
currencyCode,
direction,
@ -458,8 +464,10 @@ public class CoreApi {
marketPriceMargin,
amount,
minAmount,
buyerSecurityDeposit,
paymentAccount);
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
}
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@ -535,9 +543,11 @@ public class CoreApi {
public void takeOffer(String offerId,
String paymentAccountId,
long amountAsLong,
String challenge,
Consumer<Trade> resultHandler,
ErrorMessageHandler errorMessageHandler) {
Offer offer = coreOffersService.getOffer(offerId);
offer.setChallenge(challenge);
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
}

View file

@ -62,11 +62,12 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CoreDisputesService {
public enum DisputePayout {
// TODO: persist in DisputeResult?
public enum PayoutSuggestion {
BUYER_GETS_TRADE_AMOUNT,
BUYER_GETS_ALL, // used in desktop
BUYER_GETS_ALL,
SELLER_GETS_TRADE_AMOUNT,
SELLER_GETS_ALL, // used in desktop
SELLER_GETS_ALL,
CUSTOM
}
@ -172,17 +173,17 @@ public class CoreDisputesService {
// create dispute result
var closeDate = new Date();
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
DisputePayout payout;
PayoutSuggestion payoutSuggestion;
if (customWinnerAmount > 0) {
payout = DisputePayout.CUSTOM;
payoutSuggestion = PayoutSuggestion.CUSTOM;
} else if (winner == DisputeResult.Winner.BUYER) {
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT;
} else if (winner == DisputeResult.Winner.SELLER) {
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT;
} else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
}
applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount);
// close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
@ -227,26 +228,26 @@ public class CoreDisputesService {
* Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer
* receives the remaining amount minus the mining fee.
*/
public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
Contract contract = dispute.getContract();
Trade trade = tradeManager.getTrade(dispute.getTradeId());
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
BigInteger tradeAmount = contract.getTradeAmount();
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
} else if (payout == DisputePayout.BUYER_GETS_ALL) {
} else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
} else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) {
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
} else if (payout == DisputePayout.SELLER_GETS_ALL) {
} else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
} else if (payout == DisputePayout.CUSTOM) {
} else if (payoutSuggestion == PayoutSuggestion.CUSTOM) {
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");

View file

@ -172,10 +172,12 @@ public class CoreOffersService {
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
double securityDeposit,
double securityDepositPct,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit,
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable();
@ -199,8 +201,10 @@ public class CoreOffersService {
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
securityDeposit,
paymentAccount);
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
@ -223,8 +227,10 @@ public class CoreOffersService {
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
double buyerSecurityDeposit,
PaymentAccount paymentAccount) {
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit) {
return createOfferService.createAndGetOffer(offerId,
direction,
currencyCode.toUpperCase(),
@ -233,8 +239,10 @@ public class CoreOffersService {
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
buyerSecurityDeposit,
paymentAccount);
securityDepositPct,
paymentAccount,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
}
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {

View file

@ -132,7 +132,7 @@ class CoreTradesService {
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
String currencyCode = offer.getCurrencyCode();
OfferDirection direction = offer.getOfferPayload().getDirection();
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit());
if (offer.getPrice() != null) {
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);

View file

@ -78,6 +78,8 @@ public class OfferInfo implements Payload {
@Nullable
private final String splitOutputTxHash;
private final long splitOutputTxFee;
private final boolean isPrivateOffer;
private final String challenge;
public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId();
@ -111,6 +113,8 @@ public class OfferInfo implements Payload {
this.arbitratorSigner = builder.getArbitratorSigner();
this.splitOutputTxHash = builder.getSplitOutputTxHash();
this.splitOutputTxFee = builder.getSplitOutputTxFee();
this.isPrivateOffer = builder.isPrivateOffer();
this.challenge = builder.getChallenge();
}
public static OfferInfo toOfferInfo(Offer offer) {
@ -137,6 +141,7 @@ public class OfferInfo implements Payload {
.withIsActivated(isActivated)
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
.withChallenge(openOffer.getChallenge())
.build();
}
@ -177,7 +182,9 @@ public class OfferInfo implements Payload {
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
.withVersionNumber(offer.getOfferPayload().getVersionNr())
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress());
.withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress())
.withIsPrivateOffer(offer.isPrivateOffer())
.withChallenge(offer.getChallenge());
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -215,9 +222,11 @@ public class OfferInfo implements Payload {
.setPubKeyRing(pubKeyRing)
.setVersionNr(versionNumber)
.setProtocolVersion(protocolVersion)
.setSplitOutputTxFee(splitOutputTxFee);
.setSplitOutputTxFee(splitOutputTxFee)
.setIsPrivateOffer(isPrivateOffer);
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
return builder.build();
}
@ -255,6 +264,8 @@ public class OfferInfo implements Payload {
.withArbitratorSigner(proto.getArbitratorSigner())
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
.withIsPrivateOffer(proto.getIsPrivateOffer())
.withChallenge(proto.getChallenge())
.build();
}
}

View file

@ -172,14 +172,14 @@ public class TradeInfo implements Payload {
.withAmount(trade.getAmount().longValueExact())
.withMakerFee(trade.getMakerFee().longValueExact())
.withTakerFee(trade.getTakerFee().longValueExact())
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact())
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact())
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact())
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact())
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact())
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact())
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact())
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact())
.withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact())
.withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact())
.withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact())
.withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact())
.withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact())
.withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact())
.withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact())
.withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact())
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
.withPrice(toPreciseTradePrice.apply(trade))
.withVolume(toRoundedVolume.apply(trade))

View file

@ -63,6 +63,8 @@ public final class OfferInfoBuilder {
private String arbitratorSigner;
private String splitOutputTxHash;
private long splitOutputTxFee;
private boolean isPrivateOffer;
private String challenge;
public OfferInfoBuilder withId(String id) {
this.id = id;
@ -234,6 +236,16 @@ public final class OfferInfoBuilder {
return this;
}
public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) {
this.isPrivateOffer = isPrivateOffer;
return this;
}
public OfferInfoBuilder withChallenge(String challenge) {
this.challenge = challenge;
return this;
}
public OfferInfo build() {
return new OfferInfo(this);
}

View file

@ -33,10 +33,8 @@ import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
@ -102,9 +100,10 @@ public class CreateOfferService {
Price fixedPrice,
boolean useMarketBasedPrice,
double marketPriceMargin,
double securityDepositAsDouble,
PaymentAccount paymentAccount) {
double securityDepositPct,
PaymentAccount paymentAccount,
boolean isPrivateOffer,
boolean buyerAsTakerWithoutDeposit) {
log.info("create and get offer with offerId={}, " +
"currencyCode={}, " +
"direction={}, " +
@ -113,7 +112,9 @@ public class CreateOfferService {
"marketPriceMargin={}, " +
"amount={}, " +
"minAmount={}, " +
"securityDeposit={}",
"securityDepositPct={}, " +
"isPrivateOffer={}, " +
"buyerAsTakerWithoutDeposit={}",
offerId,
currencyCode,
direction,
@ -122,7 +123,16 @@ public class CreateOfferService {
marketPriceMargin,
amount,
minAmount,
securityDepositAsDouble);
securityDepositPct,
isPrivateOffer,
buyerAsTakerWithoutDeposit);
// verify buyer as taker security deposit
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) {
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers");
}
// verify fixed price xor market price with margin
if (fixedPrice != null) {
@ -143,10 +153,17 @@ public class CreateOfferService {
}
// adjust amount and min amount for fixed-price offer
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
if (fixedPrice != null) {
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
}
// generate one-time challenge for private offer
String challenge = null;
String challengeHash = null;
if (isPrivateOffer) {
challenge = HavenoUtils.generateChallenge();
challengeHash = HavenoUtils.getChallengeHash(challenge);
}
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
@ -161,21 +178,16 @@ public class CreateOfferService {
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
// reserved for future use cases
// Use null values if not set
boolean isPrivateOffer = false;
boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
boolean useAutoClose = false;
boolean useReOpenAfterAutoClose = false;
long lowerClosePrice = 0;
long upperClosePrice = 0;
String hashOfChallenge = null;
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
currencyCode,
direction);
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction);
offerUtil.validateOfferData(
securityDepositAsDouble,
securityDepositPct,
paymentAccount,
currencyCode);
@ -189,11 +201,11 @@ public class CreateOfferService {
useMarketBasedPriceValue,
amountAsLong,
minAmountAsLong,
HavenoUtils.MAKER_FEE_PCT,
HavenoUtils.TAKER_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
HavenoUtils.PENALTY_FEE_PCT,
securityDepositAsDouble,
securityDepositAsDouble,
hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
securityDepositPct,
baseCurrencyCode,
counterCurrencyCode,
paymentAccount.getPaymentMethod().getId(),
@ -211,7 +223,7 @@ public class CreateOfferService {
upperClosePrice,
lowerClosePrice,
isPrivateOffer,
hashOfChallenge,
challengeHash,
extraDataMap,
Version.TRADE_PROTOCOL_VERSION,
null,
@ -219,38 +231,10 @@ public class CreateOfferService {
null);
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
offer.setChallenge(challenge);
return offer;
}
public BigInteger getReservedFundsForOffer(OfferDirection direction,
BigInteger amount,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
BigInteger reservedFundsForOffer = getSecurityDeposit(direction,
amount,
buyerSecurityDeposit,
sellerSecurityDeposit);
if (!offerUtil.isBuyOffer(direction))
reservedFundsForOffer = reservedFundsForOffer.add(amount);
return reservedFundsForOffer;
}
public BigInteger getSecurityDeposit(OfferDirection direction,
BigInteger amount,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
return offerUtil.isBuyOffer(direction) ?
getBuyerSecurityDeposit(amount, buyerSecurityDeposit) :
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
}
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
Restrictions.getSellerSecurityDepositAsPercent();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
@ -259,26 +243,4 @@ public class CreateOfferService {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
}
private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) {
BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount);
return getBoundedBuyerSecurityDeposit(percentOfAmount);
}
private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) {
BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount);
return getBoundedSellerSecurityDeposit(percentOfAmount);
}
private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) {
// We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
// MinBuyerSecurityDeposit from Restrictions.
return Restrictions.getMinBuyerSecurityDeposit().max(value);
}
private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) {
// We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
// MinSellerSecurityDeposit from Restrictions.
return Restrictions.getMinSellerSecurityDeposit().max(value);
}
}

View file

@ -115,6 +115,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Setter
transient private boolean isReservedFundsSpent;
@JsonExclude
@Getter
@Setter
@Nullable
transient private String challenge;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -337,6 +343,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.getSellerSecurityDepositPct();
}
public boolean isPrivateOffer() {
return offerPayload.isPrivateOffer();
}
public String getChallengeHash() {
return offerPayload.getChallengeHash();
}
public boolean hasBuyerAsTakerWithoutDeposit() {
return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
}
public BigInteger getMaxTradeLimit() {
return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
}

View file

@ -201,7 +201,7 @@ public class OfferFilterService {
accountAgeWitnessService);
long myTradeLimit = accountOptional
.map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getMirroredDirection()))
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()))
.orElse(0L);
long offerMinAmount = offer.getMinAmount().longValueExact();
log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ",

View file

@ -156,7 +156,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// Reserved for possible future use to support private trades where the taker needs to have an accessKey
private final boolean isPrivateOffer;
@Nullable
private final String hashOfChallenge;
private final String challengeHash;
///////////////////////////////////////////////////////////////////////////////////////////
@ -195,7 +195,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
long lowerClosePrice,
long upperClosePrice,
boolean isPrivateOffer,
@Nullable String hashOfChallenge,
@Nullable String challengeHash,
@Nullable Map<String, String> extraDataMap,
int protocolVersion,
@Nullable NodeAddress arbitratorSigner,
@ -238,7 +238,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.lowerClosePrice = lowerClosePrice;
this.upperClosePrice = upperClosePrice;
this.isPrivateOffer = isPrivateOffer;
this.hashOfChallenge = hashOfChallenge;
this.challengeHash = challengeHash;
}
public byte[] getHash() {
@ -284,7 +284,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
lowerClosePrice,
upperClosePrice,
isPrivateOffer,
hashOfChallenge,
challengeHash,
extraDataMap,
protocolVersion,
arbitratorSigner,
@ -328,12 +328,17 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct());
return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted);
boolean isBuyerTaker = getDirection() == OfferDirection.SELL;
if (isPrivateOffer() && isBuyerTaker) {
return securityDepositUnadjusted;
} else {
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
}
}
public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct());
return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted);
return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -376,7 +381,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
@ -392,7 +397,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
String challengeHash = ProtoUtil.stringOrNullFromProto(proto.getChallengeHash());
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap();
@ -428,7 +433,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getLowerClosePrice(),
proto.getUpperClosePrice(),
proto.getIsPrivateOffer(),
hashOfChallenge,
challengeHash,
extraDataMapMap,
proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
@ -475,7 +480,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\r\n lowerClosePrice=" + lowerClosePrice +
",\r\n upperClosePrice=" + upperClosePrice +
",\r\n isPrivateOffer=" + isPrivateOffer +
",\r\n hashOfChallenge='" + hashOfChallenge + '\'' +
",\r\n challengeHash='" + challengeHash + '\'' +
",\r\n arbitratorSigner=" + arbitratorSigner +
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
"\r\n} ";

View file

@ -58,8 +58,8 @@ import haveno.core.trade.statistics.ReferralIdService;
import haveno.core.user.AutoConfirmSettings;
import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter;
import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
import haveno.network.p2p.P2PService;
import java.math.BigInteger;
import java.util.HashMap;
@ -120,9 +120,10 @@ public class OfferUtil {
public long getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode,
OfferDirection direction) {
OfferDirection direction,
boolean buyerAsTakerWithoutDeposit) {
return paymentAccount != null
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction)
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
: 0;
}
@ -228,16 +229,16 @@ public class OfferUtil {
return extraDataMap.isEmpty() ? null : extraDataMap;
}
public void validateOfferData(double buyerSecurityDeposit,
public void validateOfferData(double securityDeposit,
PaymentAccount paymentAccount,
String currencyCode) {
checkNotNull(p2PService.getAddress(), "Address must not be null");
checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(),
checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
"securityDeposit must not exceed " +
getMaxBuyerSecurityDepositAsPercent());
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
getMaxSecurityDepositAsPercent());
checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
"securityDeposit must not be less than " +
getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit);
getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),

View file

@ -96,6 +96,9 @@ public final class OpenOffer implements Tradable {
@Getter
private String reserveTxKey;
@Getter
@Setter
private String challenge;
@Getter
private final long triggerPrice;
@Getter
@Setter
@ -107,7 +110,6 @@ public final class OpenOffer implements Tradable {
@Getter
@Setter
transient int numProcessingAttempts = 0;
public OpenOffer(Offer offer) {
this(offer, 0, false);
}
@ -120,6 +122,7 @@ public final class OpenOffer implements Tradable {
this.offer = offer;
this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount;
this.challenge = offer.getChallenge();
state = State.PENDING;
}
@ -137,6 +140,7 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = openOffer.reserveTxHash;
this.reserveTxHex = openOffer.reserveTxHex;
this.reserveTxKey = openOffer.reserveTxKey;
this.challenge = openOffer.challenge;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -153,7 +157,8 @@ public final class OpenOffer implements Tradable {
long splitOutputTxFee,
@Nullable String reserveTxHash,
@Nullable String reserveTxHex,
@Nullable String reserveTxKey) {
@Nullable String reserveTxKey,
@Nullable String challenge) {
this.offer = offer;
this.state = state;
this.triggerPrice = triggerPrice;
@ -164,6 +169,7 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
this.challenge = challenge;
// reset reserved state to available
if (this.state == State.RESERVED) setState(State.AVAILABLE);
@ -184,6 +190,7 @@ public final class OpenOffer implements Tradable {
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
}
@ -199,7 +206,8 @@ public final class OpenOffer implements Tradable {
proto.getSplitOutputTxFee(),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()));
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
return openOffer;
}

View file

@ -79,6 +79,7 @@ import haveno.core.util.JsonUtil;
import haveno.core.util.Validator;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.BtcWalletService;
import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.core.xmr.wallet.TradeWalletService;
@ -1307,7 +1308,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress();
if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) {
errorMessage = "Cannot sign offer because we are not a registered arbitrator";
log.info(errorMessage);
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
@ -1315,47 +1316,109 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// verify arbitrator is signer of offer payload
if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) {
errorMessage = "Cannot sign offer because offer payload is for a different arbitrator";
log.info(errorMessage);
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker's trade fee
// private offers must have challenge hash
Offer offer = new Offer(request.getOfferPayload());
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) {
errorMessage = "Wrong maker fee for offer " + request.offerId;
log.info(errorMessage);
if (offer.isPrivateOffer() && (offer.getChallengeHash() == null || offer.getChallengeHash().length() == 0)) {
errorMessage = "Private offer must have challenge hash for offer " + request.offerId;
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify taker's trade fee
if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) {
errorMessage = "Wrong taker fee for offer " + request.offerId;
log.info(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
// verify maker and taker fees
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
if (hasBuyerAsTakerWithoutDeposit) {
// verify maker's trade fee
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) {
errorMessage = "Wrong maker fee for offer " + request.offerId;
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify taker's trade fee
if (offer.getTakerFeePct() != 0) {
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected 0 but got " + offer.getTakerFeePct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker security deposit
if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify taker's security deposit
if (offer.getBuyerSecurityDepositPct() != 0) {
errorMessage = "Wrong buyer security deposit for offer " + request.offerId + ". Expected 0 but got " + offer.getBuyerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
} else {
// verify maker's trade fee
if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) {
errorMessage = "Wrong maker fee for offer " + request.offerId;
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify taker's trade fee
if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) {
errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify seller's security deposit
if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify buyer's security deposit
if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// security deposits must be equal
if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) {
errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId + ": " + offer.getSellerSecurityDepositPct() + " vs " + offer.getBuyerSecurityDepositPct();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
}
// verify penalty fee
if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) {
errorMessage = "Wrong penalty fee for offer " + request.offerId;
log.info(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify security deposits are equal
if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) {
errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId;
log.info(errorMessage);
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT);
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.MAKER_FEE_PCT);
BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT);
BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
MoneroTx verifiedTx = xmrWalletService.verifyReserveTx(
@ -1710,7 +1773,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.getLowerClosePrice(),
originalOfferPayload.getUpperClosePrice(),
originalOfferPayload.isPrivateOffer(),
originalOfferPayload.getHashOfChallenge(),
originalOfferPayload.getChallengeHash(),
updatedExtraDataMap,
protocolVersion,
originalOfferPayload.getArbitratorSigner(),

View file

@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
null, // reserve tx not sent from taker to maker
null,
null,
payoutAddress);
payoutAddress,
null); // challenge is required when offer taken
// save trade request to later send to arbitrator
model.setTradeRequest(tradeRequest);

View file

@ -21,6 +21,7 @@ import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage;
@ -63,8 +64,21 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
offer.isPrivateOffer();
if (offer.isPrivateOffer()) {
boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY;
if (isBuyerMaker) {
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct());
} else {
if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
}
} else {
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
}
// We remove those checks to be more flexible with future changes.
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
@ -82,9 +96,9 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection());
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR");
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
checkNotNull(offer.getPrice(), "Price is null");

View file

@ -148,7 +148,8 @@ public class TakeOfferModel implements Model {
private long getMaxTradeLimit() {
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(),
offer.getMirroredDirection());
offer.getMirroredDirection(),
offer.hasBuyerAsTakerWithoutDeposit());
}
@NotNull

View file

@ -124,7 +124,7 @@ public class PaymentAccountUtil {
AccountAgeWitnessService accountAgeWitnessService) {
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact();
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
return !hasChargebackRisk || hasValidAccountAgeWitness;
}

View file

@ -31,6 +31,8 @@ import lombok.extern.slf4j.Slf4j;
@Singleton
public class TradeLimits {
private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that.
private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1); // max trade limit without deposit from buyer
@Nullable
@Getter
private static TradeLimits INSTANCE;
@ -57,6 +59,15 @@ public class TradeLimits {
return MAX_TRADE_LIMIT;
}
/**
* The maximum trade limit without a buyer deposit.
*
* @return the maximum trade limit for a buyer without a deposit
*/
public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() {
return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT;
}
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
// age witness is not considered anymore (> 2 months).

View file

@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator {
private ValidationResult validateIfNotTooLowPercentageValue(String input) {
try {
double percentage = ParsingUtils.parsePercentStringToDouble(input);
double minPercentage = Restrictions.getMinBuyerSecurityDepositAsPercent();
double minPercentage = Restrictions.getMinSecurityDepositAsPercent();
if (percentage < minPercentage)
return new ValidationResult(false,
Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage)));
@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator {
private ValidationResult validateIfNotTooHighPercentageValue(String input) {
try {
double percentage = ParsingUtils.parsePercentStringToDouble(input);
double maxPercentage = Restrictions.getMaxBuyerSecurityDepositAsPercent();
double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent();
if (percentage > maxPercentage)
return new ValidationResult(false,
Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage)));

View file

@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger;
import java.util.UUID;
import javax.annotation.Nullable;
/**
* Trade in the context of an arbitrator.
*/
@ -42,8 +44,9 @@ public class ArbitratorTrade extends Trade {
String uid,
NodeAddress makerNodeAddress,
NodeAddress takerNodeAddress,
NodeAddress arbitratorNodeAddress) {
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress);
NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge);
}
@Override
@ -81,7 +84,8 @@ public class ArbitratorTrade extends Trade {
uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
proto,
coreProtoResolver);
}

View file

@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger;
import java.util.UUID;
import javax.annotation.Nullable;
@Slf4j
public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
@ -43,7 +45,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
String uid,
NodeAddress makerNodeAddress,
NodeAddress takerNodeAddress,
NodeAddress arbitratorNodeAddress) {
NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer,
tradeAmount,
tradePrice,
@ -52,7 +55,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -85,7 +89,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null);
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
trade.setPrice(proto.getPrice());

View file

@ -44,7 +44,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
String uid,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer,
tradeAmount,
tradePrice,
@ -53,7 +54,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
@ -87,7 +89,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
proto,
coreProtoResolver);
}

View file

@ -38,7 +38,8 @@ public abstract class BuyerTrade extends Trade {
String uid,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer,
tradeAmount,
tradePrice,
@ -47,7 +48,8 @@ public abstract class BuyerTrade extends Trade {
uid,
takerNodeAddress,
makerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
@Override

View file

@ -36,6 +36,7 @@ package haveno.core.trade;
import com.google.protobuf.ByteString;
import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil;
import haveno.common.proto.network.NetworkPayload;
import haveno.common.util.JsonExclude;
import haveno.common.util.Utilities;
@ -53,6 +54,7 @@ import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkArgument;
@ -79,6 +81,7 @@ public final class Contract implements NetworkPayload {
private final String makerPayoutAddressString;
private final String takerPayoutAddressString;
private final String makerDepositTxHash;
@Nullable
private final String takerDepositTxHash;
public Contract(OfferPayload offerPayload,
@ -99,7 +102,7 @@ public final class Contract implements NetworkPayload {
String makerPayoutAddressString,
String takerPayoutAddressString,
String makerDepositTxHash,
String takerDepositTxHash) {
@Nullable String takerDepositTxHash) {
this.offerPayload = offerPayload;
this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice;
@ -134,6 +137,31 @@ public final class Contract implements NetworkPayload {
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.Contract toProtoMessage() {
protobuf.Contract.Builder builder = protobuf.Contract.newBuilder()
.setOfferPayload(offerPayload.toProtoMessage().getOfferPayload())
.setTradeAmount(tradeAmount)
.setTradePrice(tradePrice)
.setBuyerNodeAddress(buyerNodeAddress.toProtoMessage())
.setSellerNodeAddress(sellerNodeAddress.toProtoMessage())
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())
.setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker)
.setMakerAccountId(makerAccountId)
.setTakerAccountId(takerAccountId)
.setMakerPaymentMethodId(makerPaymentMethodId)
.setTakerPaymentMethodId(takerPaymentMethodId)
.setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash))
.setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash))
.setMakerPubKeyRing(makerPubKeyRing.toProtoMessage())
.setTakerPubKeyRing(takerPubKeyRing.toProtoMessage())
.setMakerPayoutAddressString(makerPayoutAddressString)
.setTakerPayoutAddressString(takerPayoutAddressString)
.setMakerDepositTxHash(makerDepositTxHash);
Optional.ofNullable(takerDepositTxHash).ifPresent(builder::setTakerDepositTxHash);
return builder.build();
}
public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) {
return new Contract(OfferPayload.fromProto(proto.getOfferPayload()),
proto.getTradeAmount(),
@ -153,32 +181,7 @@ public final class Contract implements NetworkPayload {
proto.getMakerPayoutAddressString(),
proto.getTakerPayoutAddressString(),
proto.getMakerDepositTxHash(),
proto.getTakerDepositTxHash());
}
@Override
public protobuf.Contract toProtoMessage() {
return protobuf.Contract.newBuilder()
.setOfferPayload(offerPayload.toProtoMessage().getOfferPayload())
.setTradeAmount(tradeAmount)
.setTradePrice(tradePrice)
.setBuyerNodeAddress(buyerNodeAddress.toProtoMessage())
.setSellerNodeAddress(sellerNodeAddress.toProtoMessage())
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())
.setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker)
.setMakerAccountId(makerAccountId)
.setTakerAccountId(takerAccountId)
.setMakerPaymentMethodId(makerPaymentMethodId)
.setTakerPaymentMethodId(takerPaymentMethodId)
.setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash))
.setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash))
.setMakerPubKeyRing(makerPubKeyRing.toProtoMessage())
.setTakerPubKeyRing(takerPubKeyRing.toProtoMessage())
.setMakerPayoutAddressString(makerPayoutAddressString)
.setTakerPayoutAddressString(takerPayoutAddressString)
.setMakerDepositTxHash(makerDepositTxHash)
.setTakerDepositTxHash(takerDepositTxHash)
.build();
ProtoUtil.stringOrNullFromProto(proto.getTakerDepositTxHash()));
}

View file

@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.crypto.Sig;
import haveno.common.file.FileUtil;
import haveno.common.util.Base64;
import haveno.common.util.Utilities;
import haveno.core.api.CoreNotificationService;
import haveno.core.api.XmrConnectionService;
@ -48,7 +49,10 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
@ -87,13 +91,15 @@ public class HavenoUtils {
// configure fees
public static final boolean ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS = true;
public static final double PENALTY_FEE_PCT = 0.02; // 2%
public static final double MAKER_FEE_PCT = 0.0015; // 0.15%
public static final double TAKER_FEE_PCT = 0.0075; // 0.75%
public static final double PENALTY_FEE_PCT = 0.02; // 2%
public static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT = MAKER_FEE_PCT + TAKER_FEE_PCT; // customize maker's fee when no deposit or fee from taker
// other configuration
public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes
public static final long LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS = 1000 * 30; // log warnings when daemon not synced once every 30s
public static final int PRIVATE_OFFER_PASSPHRASE_NUM_WORDS = 8; // number of words in a private offer passphrase
// synchronize requests to the daemon
private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors?
@ -286,6 +292,41 @@ public class HavenoUtils {
// ------------------------ SIGNING AND VERIFYING -------------------------
public static String generateChallenge() {
try {
// load bip39 words
String fileName = "bip39_english.txt";
File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName);
if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File);
List<String> bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8);
// select words randomly
List<String> passphraseWords = new ArrayList<String>();
SecureRandom secureRandom = new SecureRandom();
for (int i = 0; i < PRIVATE_OFFER_PASSPHRASE_NUM_WORDS; i++) {
passphraseWords.add(bip39Words.get(secureRandom.nextInt(bip39Words.size())));
}
return String.join(" ", passphraseWords);
} catch (Exception e) {
throw new IllegalStateException("Failed to generate challenge", e);
}
}
public static String getChallengeHash(String challenge) {
if (challenge == null) return null;
// tokenize passphrase
String[] words = challenge.toLowerCase().split(" ");
// collect first 4 letters of each word, which are unique in bip39
List<String> prefixes = new ArrayList<String>();
for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4)));
// hash the result
return Base64.encode(Hash.getSha256Hash(String.join(" ", prefixes).getBytes()));
}
public static byte[] sign(KeyRing keyRing, String message) {
return sign(keyRing.getSignatureKeyPair().getPrivate(), message);
}

View file

@ -44,7 +44,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
String uid,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer,
tradeAmount,
tradePrice,
@ -53,7 +54,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
@ -87,7 +89,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null);
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
trade.setPrice(proto.getPrice());

View file

@ -44,7 +44,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
String uid,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer,
tradeAmount,
tradePrice,
@ -53,7 +54,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
@ -87,7 +89,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
uid,
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
proto,
coreProtoResolver);
}

View file

@ -36,7 +36,8 @@ public abstract class SellerTrade extends Trade {
String uid,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super(offer,
tradeAmount,
tradePrice,
@ -45,7 +46,8 @@ public abstract class SellerTrade extends Trade {
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
@Override

View file

@ -486,6 +486,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
private IdlePayoutSyncer idlePayoutSyncer;
@Getter
private boolean isCompleted;
@Getter
private final String challenge;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructors
@ -500,7 +502,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
String uid,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
super();
this.offer = offer;
this.amount = tradeAmount.longValueExact();
@ -511,6 +514,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
this.uid = uid;
this.takeOfferDate = new Date().getTime();
this.tradeListeners = new ArrayList<TradeListener>();
this.challenge = challenge;
getMaker().setNodeAddress(makerNodeAddress);
getTaker().setNodeAddress(takerNodeAddress);
@ -534,7 +538,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
String uid,
@Nullable NodeAddress makerNodeAddress,
@Nullable NodeAddress takerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress) {
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable String challenge) {
this(offer,
tradeAmount,
@ -544,7 +549,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
}
// TODO: remove these constructors
@ -559,7 +565,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
NodeAddress arbitratorNodeAddress,
XmrWalletService xmrWalletService,
ProcessModel processModel,
String uid) {
String uid,
@Nullable String challenge) {
this(offer,
tradeAmount,
@ -569,7 +576,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
uid,
makerNodeAddress,
takerNodeAddress,
arbitratorNodeAddress);
arbitratorNodeAddress,
challenge);
setAmount(tradeAmount);
}
@ -1233,7 +1241,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null");
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null");
BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount();
BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount();
BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount();
BigInteger tradeAmount = getAmount();
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
@ -1324,7 +1332,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
MoneroWallet wallet = getWallet();
Contract contract = getContract();
BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount();
BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount();
BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount();
BigInteger tradeAmount = getAmount();
// describe payout tx
@ -2091,9 +2099,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
final long tradeTime = getTakeOfferDate().getTime();
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod");
if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?");
if (getMakerDepositTx() == null || (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?");
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
long maxHeight = Math.max(getMakerDepositTx().getHeight(), hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight());
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
// If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date.
@ -2125,7 +2133,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
public boolean isDepositsPublished() {
if (isDepositFailed()) return false;
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null;
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (getTaker().getDepositTxHash() != null || hasBuyerAsTakerWithoutDeposit());
}
public boolean isFundsLockedIn() {
@ -2277,7 +2285,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
public BigInteger getTakerFee() {
return offer.getTakerFee(getAmount());
return hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : offer.getTakerFee(getAmount());
}
public BigInteger getSecurityDepositBeforeMiningFee() {
return isBuyer() ? getBuyerSecurityDepositBeforeMiningFee() : getSellerSecurityDepositBeforeMiningFee();
}
public BigInteger getBuyerSecurityDepositBeforeMiningFee() {
@ -2288,6 +2300,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(getAmount());
}
public boolean isBuyerAsTakerWithoutDeposit() {
return isBuyer() && isTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee());
}
public boolean hasBuyerAsTakerWithoutDeposit() {
return getBuyer() == getTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee());
}
@Override
public BigInteger getTotalTxFee() {
return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees
@ -2303,7 +2323,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
public boolean isTxChainInvalid() {
return processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null;
return processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit());
}
/**
@ -2537,7 +2557,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (isPayoutUnlocked()) return;
// skip if deposit txs unknown or not requested
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return;
if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return;
// skip if daemon not synced
if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return;
@ -2553,7 +2573,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// get txs from trade wallet
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && hasBuyerAsTakerWithoutDeposit()));
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
List<MoneroTxWallet> txs;
if (!updatePool) txs = wallet.getTxs(query);
@ -2565,22 +2585,22 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
setDepositTxs(txs);
if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen
if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen
setStateDepositsSeen();
// set actual security deposits
if (getBuyer().getSecurityDeposit().longValueExact() == 0) {
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
BigInteger buyerSecurityDeposit = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount());
getBuyer().setSecurityDeposit(buyerSecurityDeposit);
getSeller().setSecurityDeposit(sellerSecurityDeposit);
}
// check for deposit txs confirmation
if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed();
if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) setStateDepositsConfirmed();
// check for deposit txs unlocked
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) {
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) {
setStateDepositsUnlocked();
}
}
@ -2750,7 +2770,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId());
return true;
}
if (getTakerDepositTx() == null) {
if (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()) {
log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId());
return true;
}
@ -2913,6 +2933,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
return builder.build();
}
@ -2982,6 +3003,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
",\n refundResultState=" + refundResultState +
",\n refundResultStateProperty=" + refundResultStateProperty +
",\n isCompleted=" + isCompleted +
",\n challenge='" + challenge + '\'' +
"\n}";
}
}

View file

@ -561,6 +561,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
OpenOffer openOffer = openOfferOptional.get();
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
Offer offer = openOffer.getOffer();
// validate challenge
if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) {
log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender);
return;
}
// ensure trade does not already exist
Optional<Trade> tradeOptional = getOpenTrade(request.getOfferId());
@ -583,7 +589,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
request.getMakerNodeAddress(),
request.getTakerNodeAddress(),
request.getArbitratorNodeAddress());
request.getArbitratorNodeAddress(),
openOffer.getChallenge());
else
trade = new SellerAsMakerTrade(offer,
BigInteger.valueOf(request.getTradeAmount()),
@ -593,7 +600,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
request.getMakerNodeAddress(),
request.getTakerNodeAddress(),
request.getArbitratorNodeAddress());
request.getArbitratorNodeAddress(),
openOffer.getChallenge());
trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId());
trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId());
trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing());
@ -646,6 +654,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
// validate challenge hash
if (offer.getChallengeHash() != null && !offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(request.getChallenge()))) {
log.warn("Ignoring InitTradeRequest to arbitrator because challenge hash is incorrect, tradeId={}, sender={}", request.getOfferId(), sender);
return;
}
// handle trade
Trade trade;
Optional<Trade> tradeOptional = getOpenTrade(offer.getId());
@ -679,7 +693,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
request.getMakerNodeAddress(),
request.getTakerNodeAddress(),
request.getArbitratorNodeAddress());
request.getArbitratorNodeAddress(),
request.getChallenge());
// set reserve tx hash if available
Optional<SignedOffer> signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId());
@ -873,7 +888,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
offer.getMakerNodeAddress(),
P2PService.getMyNodeAddress(),
null);
null,
offer.getChallenge());
} else {
trade = new BuyerAsTakerTrade(offer,
amount,
@ -883,7 +899,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
UUID.randomUUID().toString(),
offer.getMakerNodeAddress(),
P2PService.getMyNodeAddress(),
null);
null,
offer.getChallenge());
}
trade.getProcessModel().setUseSavingsWallet(useSavingsWallet);
trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact());
@ -1127,7 +1144,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
log.warn("We found a closed trade with locked up funds. " +
"That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
}
} else {
} else if (!trade.hasBuyerAsTakerWithoutDeposit()) {
log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
}

View file

@ -33,7 +33,9 @@ import java.util.Optional;
public final class DepositRequest extends TradeMessage implements DirectMessage {
private final long currentDate;
private final byte[] contractSignature;
@Nullable
private final String depositTxHex;
@Nullable
private final String depositTxKey;
@Nullable
private final byte[] paymentAccountKey;
@ -43,8 +45,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage
String messageVersion,
long currentDate,
byte[] contractSignature,
String depositTxHex,
String depositTxKey,
@Nullable String depositTxHex,
@Nullable String depositTxKey,
@Nullable byte[] paymentAccountKey) {
super(messageVersion, tradeId, uid);
this.currentDate = currentDate;
@ -63,13 +65,12 @@ public final class DepositRequest extends TradeMessage implements DirectMessage
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.DepositRequest.Builder builder = protobuf.DepositRequest.newBuilder()
.setTradeId(offerId)
.setUid(uid)
.setDepositTxHex(depositTxHex)
.setDepositTxKey(depositTxKey);
.setUid(uid);
builder.setCurrentDate(currentDate);
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
Optional.ofNullable(depositTxHex).ifPresent(builder::setDepositTxHex);
Optional.ofNullable(depositTxKey).ifPresent(builder::setDepositTxKey);
Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e)));
return getNetworkEnvelopeBuilder().setDepositRequest(builder).build();
}
@ -81,8 +82,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage
messageVersion,
proto.getCurrentDate(),
ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()),
proto.getDepositTxHex(),
proto.getDepositTxKey(),
ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()),
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()));
}

View file

@ -58,6 +58,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
private final String reserveTxKey;
@Nullable
private final String payoutAddress;
@Nullable
private final String challenge;
public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion,
String offerId,
@ -79,7 +81,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
@Nullable String reserveTxHash,
@Nullable String reserveTxHex,
@Nullable String reserveTxKey,
@Nullable String payoutAddress) {
@Nullable String payoutAddress,
@Nullable String challenge) {
super(messageVersion, offerId, uid);
this.tradeProtocolVersion = tradeProtocolVersion;
this.tradeAmount = tradeAmount;
@ -99,6 +102,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
this.payoutAddress = payoutAddress;
this.challenge = challenge;
}
@ -129,6 +133,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress));
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e)));
builder.setCurrentDate(currentDate);
@ -158,7 +163,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()));
ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()),
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
}
@Override
@ -183,6 +189,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
",\n reserveTxHex=" + reserveTxHex +
",\n reserveTxKey=" + reserveTxKey +
",\n payoutAddress=" + payoutAddress +
",\n challenge=" + challenge +
"\n} " + super.toString();
}
}

View file

@ -35,7 +35,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
private final String accountId;
private final byte[] paymentAccountPayloadHash;
private final String payoutAddress;
@Nullable
private final String depositTxHash;
@Nullable
private final byte[] accountAgeWitnessSignatureOfDepositHash;
public SignContractRequest(String tradeId,
@ -45,7 +47,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
String accountId,
byte[] paymentAccountPayloadHash,
String payoutAddress,
String depositTxHash,
@Nullable String depositTxHash,
@Nullable byte[] accountAgeWitnessSignatureOfDepositHash) {
super(messageVersion, tradeId, uid);
this.currentDate = currentDate;
@ -68,10 +70,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
.setUid(uid)
.setAccountId(accountId)
.setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash))
.setPayoutAddress(payoutAddress)
.setDepositTxHash(depositTxHash);
.setPayoutAddress(payoutAddress);
Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e)));
Optional.ofNullable(depositTxHash).ifPresent(builder::setDepositTxHash);
builder.setCurrentDate(currentDate);
return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build();
@ -87,7 +88,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
proto.getAccountId(),
proto.getPaymentAccountPayloadHash().toByteArray(),
proto.getPayoutAddress(),
proto.getDepositTxHash(),
ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()),
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash()));
}

View file

@ -158,7 +158,6 @@ public final class TradePeer implements PersistablePayload {
}
public BigInteger getSecurityDeposit() {
if (depositTxHash == null) return null;
return BigInteger.valueOf(securityDeposit);
}

View file

@ -36,8 +36,9 @@ import monero.daemon.model.MoneroSubmitTxResult;
import monero.daemon.model.MoneroTx;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Slf4j
@ -83,72 +84,86 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
byte[] signature = request.getContractSignature();
// get trader info
TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (trader == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator");
PubKeyRing peerPubKeyRing = trader.getPubKeyRing();
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
if (sender == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator");
PubKeyRing senderPubKeyRing = sender.getPubKeyRing();
// verify signature
if (!HavenoUtils.isSignatureValid(peerPubKeyRing, contractAsJson, signature)) {
if (!HavenoUtils.isSignatureValid(senderPubKeyRing, contractAsJson, signature)) {
throw new RuntimeException("Peer's contract signature is invalid");
}
// set peer's signature
trader.setContractSignature(signature);
sender.setContractSignature(signature);
// collect expected values
Offer offer = trade.getOffer();
boolean isFromTaker = trader == trade.getTaker();
boolean isFromBuyer = trader == trade.getBuyer();
boolean isFromTaker = sender == trade.getTaker();
boolean isFromBuyer = sender == trade.getBuyer();
BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee();
BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount();
BigInteger securityDeposit = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
String depositAddress = processModel.getMultisigAddress();
sender.setSecurityDeposit(securityDeposit);
// verify deposit tx
MoneroTx verifiedTx;
try {
verifiedTx = trade.getXmrWalletService().verifyDepositTx(
offer.getId(),
tradeFee,
trade.getProcessModel().getTradeFeeAddress(),
sendTradeAmount,
securityDeposit,
depositAddress,
trader.getDepositTxHash(),
request.getDepositTxHex(),
request.getDepositTxKey(),
null);
} catch (Exception e) {
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit();
if (!isFromBuyerAsTakerWithoutDeposit) {
MoneroTx verifiedTx;
try {
verifiedTx = trade.getXmrWalletService().verifyDepositTx(
offer.getId(),
tradeFee,
trade.getProcessModel().getTradeFeeAddress(),
sendTradeAmount,
securityDeposit,
depositAddress,
sender.getDepositTxHash(),
request.getDepositTxHex(),
request.getDepositTxKey(),
null);
} catch (Exception e) {
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + sender.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// update trade state
sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
sender.setDepositTxFee(verifiedTx.getFee());
sender.setDepositTxHex(request.getDepositTxHex());
sender.setDepositTxKey(request.getDepositTxKey());
}
// update trade state
trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
trader.setDepositTxFee(verifiedTx.getFee());
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
if (request.getPaymentAccountKey() != null) sender.setPaymentAccountKey(request.getPaymentAccountKey());
processModel.getTradeManager().requestPersistence();
// relay deposit txs when both available
// relay deposit txs when both requests received
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
// check timeout and extend just before relaying
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId());
trade.addInitProgressStep();
// relay deposit txs
boolean depositTxsRelayed = false;
List<String> txHashes = new ArrayList<>();
try {
// submit txs to pool but do not relay
// submit maker tx to pool but do not relay
MoneroSubmitTxResult makerResult = daemon.submitTxHex(processModel.getMaker().getDepositTxHex(), true);
MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true);
if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult));
if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult));
txHashes.add(processModel.getMaker().getDepositTxHash());
// submit taker tx to pool but do not relay
if (!trade.hasBuyerAsTakerWithoutDeposit()) {
MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true);
if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult));
txHashes.add(processModel.getTaker().getDepositTxHash());
}
// relay txs
daemon.relayTxsByHash(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()));
daemon.relayTxsByHash(txHashes);
depositTxsRelayed = true;
// update trade state
@ -160,7 +175,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// flush txs from pool
try {
daemon.flushTxPool(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash());
daemon.flushTxPool(txHashes);
} catch (Exception e2) {
log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2);
}
@ -180,7 +195,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
});
if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId());
if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
}
}

View file

@ -53,38 +53,44 @@ public class ArbitratorProcessReserveTx extends TradeTask {
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
boolean isFromMaker = sender == trade.getMaker();
boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL;
sender = isFromMaker ? processModel.getMaker() : processModel.getTaker();
BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
sender.setSecurityDeposit(securityDeposit);
// TODO (woodser): if signer online, should never be called by maker?
// process reserve tx with expected values
BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct());
BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee();
BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount
BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
MoneroTx verifiedTx;
try {
verifiedTx = trade.getXmrWalletService().verifyReserveTx(
offer.getId(),
penaltyFee,
tradeFee,
sendAmount,
securityDeposit,
request.getPayoutAddress(),
request.getReserveTxHash(),
request.getReserveTxHex(),
request.getReserveTxKey(),
null);
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// process reserve tx unless from buyer as taker without deposit
boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && !isFromMaker && trade.hasBuyerAsTakerWithoutDeposit();
if (!isFromBuyerAsTakerWithoutDeposit) {
// save reserve tx to model
TradePeer trader = isFromMaker ? processModel.getMaker() : processModel.getTaker();
trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
trader.setReserveTxHash(request.getReserveTxHash());
trader.setReserveTxHex(request.getReserveTxHex());
trader.setReserveTxKey(request.getReserveTxKey());
// process reserve tx with expected values
BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct());
BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee();
BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount
MoneroTx verifiedTx;
try {
verifiedTx = trade.getXmrWalletService().verifyReserveTx(
offer.getId(),
penaltyFee,
tradeFee,
sendAmount,
securityDeposit,
request.getPayoutAddress(),
request.getReserveTxHash(),
request.getReserveTxHex(),
request.getReserveTxKey(),
null);
} catch (Exception e) {
log.error(ExceptionUtils.getStackTrace(e));
throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
}
// save reserve tx to model
sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
sender.setReserveTxHash(request.getReserveTxHash());
sender.setReserveTxHex(request.getReserveTxHex());
sender.setReserveTxKey(request.getReserveTxKey());
}
// persist trade
processModel.getTradeManager().requestPersistence();

View file

@ -78,6 +78,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
null,
null,
null,
null,
null);
// send request to taker
@ -118,7 +119,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
// ensure arbitrator has reserve txs
if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade");
if (processModel.getTaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade");
if (processModel.getTaker().getReserveTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade");
// create wallet for multisig
MoneroWallet multisigWallet = trade.createWallet();

View file

@ -74,7 +74,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null");
Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null");
Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null");
Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
if (!trade.hasBuyerAsTakerWithoutDeposit()) Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
checkNotNull(trade.getOffer(), "offer must not be null");
// create payout tx if we have seller's updated multisig hex

View file

@ -138,7 +138,8 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask {
trade.getSelf().getReserveTxHash(),
trade.getSelf().getReserveTxHex(),
trade.getSelf().getReserveTxKey(),
model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString());
model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(),
trade.getChallenge());
// send request to arbitrator
log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress());

View file

@ -83,7 +83,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId());
trade.startProtocolTimeout();
// collect relevant info
// collect info
Integer subaddressIndex = null;
boolean reserveExactAmount = false;
if (trade instanceof MakerTrade) {
@ -97,53 +97,60 @@ public class MaybeSendSignContractRequest extends TradeTask {
}
// attempt creating deposit tx
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) {
log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (!trade.isBuyerAsTakerWithoutDeposit()) {
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) {
log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
if (depositTx != null) break;
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (depositTx != null) break;
}
} catch (Exception e) {
// thaw deposit inputs
if (depositTx != null) {
trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx));
trade.getSelf().setReserveTxKeyImages(null);
}
// re-freeze maker offer inputs
if (trade instanceof MakerTrade) {
trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages());
}
throw e;
}
} catch (Exception e) {
// thaw deposit inputs
if (depositTx != null) {
trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx));
trade.getSelf().setReserveTxKeyImages(null);
}
// re-freeze maker offer inputs
if (trade instanceof MakerTrade) {
trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages());
}
throw e;
}
// reset protocol timeout
trade.addInitProgressStep();
// update trade state
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee());
trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx));
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
trade.getSelf().setPaymentAccountPayloadHash(trade.getSelf().getPaymentAccountPayload().getHash());
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
if (depositTx == null) {
trade.getSelf().setSecurityDeposit(securityDeposit);
} else {
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee());
trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx));
}
}
// maker signs deposit hash nonce to avoid challenge protocol
@ -161,7 +168,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
trade.getProcessModel().getAccountId(),
trade.getSelf().getPaymentAccountPayload().getHash(),
trade.getSelf().getPayoutAddressString(),
depositTx.getHash(),
depositTx == null ? null : depositTx.getHash(),
sig);
// send request to trading peer

View file

@ -63,20 +63,20 @@ public class ProcessSignContractRequest extends TradeTask {
// extract fields from request
// TODO (woodser): verify request and from maker or taker
SignContractRequest request = (SignContractRequest) processModel.getTradeMessage();
TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
trader.setDepositTxHash(request.getDepositTxHash());
trader.setAccountId(request.getAccountId());
trader.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash());
trader.setPayoutAddressString(request.getPayoutAddress());
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
sender.setDepositTxHash(request.getDepositTxHash());
sender.setAccountId(request.getAccountId());
sender.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash());
sender.setPayoutAddressString(request.getPayoutAddress());
// maker sends witness signature of deposit tx hash
if (trader == trade.getMaker()) {
trader.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8));
trader.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash());
if (sender == trade.getMaker()) {
sender.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8));
sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash());
}
// sign contract only when both deposit txs hashes known
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) {
// sign contract only when received from both peers
if (processModel.getMaker().getPaymentAccountPayloadHash() == null || processModel.getTaker().getPaymentAccountPayloadHash() == null) {
complete();
return;
}

View file

@ -82,8 +82,8 @@ public class SendDepositRequest extends TradeTask {
Version.getP2PMessageVersion(),
new Date().getTime(),
trade.getSelf().getContractSignature(),
trade.getSelf().getDepositTx().getFullHex(),
trade.getSelf().getDepositTx().getKey(),
trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(),
trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(),
trade.getSelf().getPaymentAccountKey());
// update trade state

View file

@ -47,62 +47,63 @@ public class TakerReserveTradeFunds extends TradeTask {
throw new RuntimeException("Expected taker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen.");
}
// create reserve tx
// create reserve tx unless deposit not required from buyer as taker
MoneroTxWallet reserveTx = null;
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
if (!trade.isBuyerAsTakerWithoutDeposit()) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
trade.startProtocolTimeout();
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
trade.startProtocolTimeout();
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
BigInteger takerFee = trade.getTakerFee();
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
BigInteger takerFee = trade.getTakerFee();
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
BigInteger securityDeposit = trade.getSecurityDepositBeforeMiningFee();
String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
// attempt creating reserve tx
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) {
log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
// attempt creating reserve tx
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) {
log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
if (reserveTx != null) break;
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (reserveTx != null) break;
}
}
} catch (Exception e) {
} catch (Exception e) {
// reset state with wallet lock
model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId());
if (reserveTx != null) {
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
trade.getSelf().setReserveTxKeyImages(null);
// reset state with wallet lock
model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId());
if (reserveTx != null) {
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
trade.getSelf().setReserveTxKeyImages(null);
}
throw e;
}
throw e;
// reset protocol timeout
trade.startProtocolTimeout();
// update trade state
trade.getTaker().setReserveTxHash(reserveTx.getHash());
trade.getTaker().setReserveTxHex(reserveTx.getFullHex());
trade.getTaker().setReserveTxKey(reserveTx.getKey());
trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
}
// reset protocol timeout
trade.startProtocolTimeout();
// update trade state
trade.getTaker().setReserveTxHash(reserveTx.getHash());
trade.getTaker().setReserveTxHex(reserveTx.getFullHex());
trade.getTaker().setReserveTxKey(reserveTx.getKey());
trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
}
// save process state

View file

@ -48,7 +48,9 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker
checkNotNull(sourceRequest);
checkTradeId(processModel.getOfferId(), sourceRequest);
if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) {
throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
}
// create request to arbitrator
Offer offer = processModel.getOffer();
@ -73,7 +75,8 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
trade.getSelf().getReserveTxHash(),
trade.getSelf().getReserveTxHex(),
trade.getSelf().getReserveTxKey(),
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString());
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
trade.getChallenge());
// send request to arbitrator
log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress());

View file

@ -47,7 +47,9 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask {
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());
if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) {
throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
}
// collect fields
Offer offer = model.getOffer();
@ -55,6 +57,7 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask {
P2PService p2PService = processModel.getP2PService();
XmrWalletService walletService = model.getXmrWalletService();
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
String challenge = model.getChallenge();
// taker signs offer using offer id as nonce to avoid challenge protocol
byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId());
@ -81,7 +84,8 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask {
null, // reserve tx not sent from taker to maker
null,
null,
payoutAddress);
payoutAddress,
challenge);
// send request to maker
log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress());

View file

@ -616,14 +616,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence();
}
public void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent, PaymentAccount paymentAccount) {
double max = Restrictions.getMaxBuyerSecurityDepositAsPercent();
double min = Restrictions.getMinBuyerSecurityDepositAsPercent();
public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) {
double max = Restrictions.getMaxSecurityDepositAsPercent();
double min = Restrictions.getMinSecurityDepositAsPercent();
if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount))
prefPayload.setBuyerSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent)));
prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent)));
else
prefPayload.setBuyerSecurityDepositAsPercent(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent)));
prefPayload.setSecurityDepositAsPercent(Math.min(max, Math.max(min, securityDepositAsPercent)));
requestPersistence();
}
@ -755,6 +755,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence();
}
public void setShowPrivateOffers(boolean value) {
prefPayload.setShowPrivateOffers(value);
requestPersistence();
}
public void setDenyApiTaker(boolean value) {
prefPayload.setDenyApiTaker(value);
requestPersistence();
@ -838,16 +843,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
return prefPayload.isSplitOfferOutput();
}
public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) {
public double getSecurityDepositAsPercent(PaymentAccount paymentAccount) {
double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ?
prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent();
prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent();
if (value < Restrictions.getMinBuyerSecurityDepositAsPercent()) {
value = Restrictions.getMinBuyerSecurityDepositAsPercent();
setBuyerSecurityDepositAsPercent(value, paymentAccount);
if (value < Restrictions.getMinSecurityDepositAsPercent()) {
value = Restrictions.getMinSecurityDepositAsPercent();
setSecurityDepositAsPercent(value, paymentAccount);
}
return value == 0 ? Restrictions.getDefaultBuyerSecurityDepositAsPercent() : value;
return value == 0 ? Restrictions.getDefaultSecurityDepositAsPercent() : value;
}
@Override

View file

@ -41,7 +41,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
@Slf4j
@Data
@ -120,10 +120,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
private String rpcPw;
@Nullable
private String takeOfferSelectedPaymentAccountId;
private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent();
private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent();
private int ignoreDustThreshold = 600;
private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL;
private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent();
private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent();
private int blockNotifyPort;
private boolean tacAcceptedV120;
private double bsqAverageTrimThreshold = 0.05;
@ -134,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
// Added in 1.5.5
private boolean hideNonAccountPaymentMethods;
private boolean showOffersMatchingMyAccounts;
private boolean showPrivateOffers;
private boolean denyApiTaker;
private boolean notifyOnPreRelease;
@ -193,10 +194,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
.setUseStandbyMode(useStandbyMode)
.setUseSoundForNotifications(useSoundForNotifications)
.setUseSoundForNotificationsInitialized(useSoundForNotificationsInitialized)
.setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent)
.setSecurityDepositAsPercent(securityDepositAsPercent)
.setIgnoreDustThreshold(ignoreDustThreshold)
.setClearDataAfterDays(clearDataAfterDays)
.setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto)
.setSecurityDepositAsPercentForCrypto(securityDepositAsPercentForCrypto)
.setBlockNotifyPort(blockNotifyPort)
.setTacAcceptedV120(tacAcceptedV120)
.setBsqAverageTrimThreshold(bsqAverageTrimThreshold)
@ -205,6 +206,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
.collect(Collectors.toList()))
.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods)
.setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts)
.setShowPrivateOffers(showPrivateOffers)
.setDenyApiTaker(denyApiTaker)
.setNotifyOnPreRelease(notifyOnPreRelease);
@ -297,10 +299,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(),
proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(),
proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(),
proto.getBuyerSecurityDepositAsPercent(),
proto.getSecurityDepositAsPercent(),
proto.getIgnoreDustThreshold(),
proto.getClearDataAfterDays(),
proto.getBuyerSecurityDepositAsPercentForCrypto(),
proto.getSecurityDepositAsPercentForCrypto(),
proto.getBlockNotifyPort(),
proto.getTacAcceptedV120(),
proto.getBsqAverageTrimThreshold(),
@ -310,6 +312,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
.collect(Collectors.toList())),
proto.getHideNonAccountPaymentMethods(),
proto.getShowOffersMatchingMyAccounts(),
proto.getShowPrivateOffers(),
proto.getDenyApiTaker(),
proto.getNotifyOnPreRelease(),
XmrNodeSettings.fromProto(proto.getXmrNodeSettings())

View file

@ -47,35 +47,35 @@ public class CoinUtil {
}
/**
* @param value Btc amount to be converted to percent value. E.g. 0.01 BTC is 1% (of 1 BTC)
* @param value Xmr amount to be converted to percent value. E.g. 0.01 XMR is 1% (of 1 XMR)
* @return The percentage value as double (e.g. 1% is 0.01)
*/
public static double getAsPercentPerBtc(BigInteger value) {
return getAsPercentPerBtc(value, HavenoUtils.xmrToAtomicUnits(1.0));
public static double getAsPercentPerXmr(BigInteger value) {
return getAsPercentPerXmr(value, HavenoUtils.xmrToAtomicUnits(1.0));
}
/**
* @param part Btc amount to be converted to percent value, based on total value passed.
* E.g. 0.1 BTC is 25% (of 0.4 BTC)
* @param total Total Btc amount the percentage part is calculated from
* @param part Xmr amount to be converted to percent value, based on total value passed.
* E.g. 0.1 XMR is 25% (of 0.4 XMR)
* @param total Total Xmr amount the percentage part is calculated from
*
* @return The percentage value as double (e.g. 1% is 0.01)
*/
public static double getAsPercentPerBtc(BigInteger part, BigInteger total) {
public static double getAsPercentPerXmr(BigInteger part, BigInteger total) {
return MathUtils.roundDouble(HavenoUtils.divide(part == null ? BigInteger.ZERO : part, total == null ? BigInteger.valueOf(1) : total), 4);
}
/**
* @param percent The percentage value as double (e.g. 1% is 0.01)
* @param amount The amount as atomic units for the percentage calculation
* @return The percentage as atomic units (e.g. 1% of 1 BTC is 0.01 BTC)
* @return The percentage as atomic units (e.g. 1% of 1 XMR is 0.01 XMR)
*/
public static BigInteger getPercentOfAmount(double percent, BigInteger amount) {
if (amount == null) amount = BigInteger.ZERO;
return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger();
}
public static BigInteger getRoundedAmount(BigInteger amount, Price price, long maxTradeLimit, String currencyCode, String paymentMethodId) {
public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) {
if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) {
return getRoundedAtmCashAmount(amount, price, maxTradeLimit);
} else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) {
@ -86,7 +86,7 @@ public class CoinUtil {
return amount;
}
public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, long maxTradeLimit) {
public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) {
return getAdjustedAmount(amount, price, maxTradeLimit, 10);
}
@ -99,11 +99,11 @@ public class CoinUtil {
* @param maxTradeLimit The max. trade limit of the users account, in atomic units.
* @return The adjusted amount
*/
public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, long maxTradeLimit) {
public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) {
return getAdjustedAmount(amount, price, maxTradeLimit, 1);
}
public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, long maxTradeLimit) {
public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, Long maxTradeLimit) {
DecimalFormat decimalFormat = new DecimalFormat("#.####");
double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount)));
return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount);
@ -121,7 +121,7 @@ public class CoinUtil {
* @return The adjusted amount
*/
@VisibleForTesting
static BigInteger getAdjustedAmount(BigInteger amount, Price price, long maxTradeLimit, int factor) {
static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) {
checkArgument(
amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(),
"amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr"
@ -163,11 +163,13 @@ public class CoinUtil {
// If we are above our trade limit we reduce the amount by the smallestUnitForAmount
BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume);
while (adjustedAmount > maxTradeLimit) {
adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact();
if (maxTradeLimit != null) {
while (adjustedAmount > maxTradeLimit) {
adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact();
}
}
adjustedAmount = Math.max(minTradeAmount, adjustedAmount);
adjustedAmount = Math.min(maxTradeLimit, adjustedAmount);
if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, adjustedAmount);
return BigInteger.valueOf(adjustedAmount);
}
}

View file

@ -24,11 +24,13 @@ import org.bitcoinj.core.Coin;
import java.math.BigInteger;
public class Restrictions {
// configure restrictions
public static final double MIN_SECURITY_DEPOSIT_PCT = 0.15;
public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5;
public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1);
public static BigInteger MIN_BUYER_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1);
// For the seller we use a fixed one as there is no way the seller can cancel the trade
// To make it editable would just increase complexity.
public static BigInteger MIN_SELLER_SECURITY_DEPOSIT = MIN_BUYER_SECURITY_DEPOSIT;
public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1);
// At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the
// mediated payout. For Refund agent cases we do not have that restriction.
private static BigInteger MIN_REFUND_AT_MEDIATED_DISPUTE;
@ -53,31 +55,20 @@ public class Restrictions {
return MIN_TRADE_AMOUNT;
}
public static double getDefaultBuyerSecurityDepositAsPercent() {
return 0.15; // 15% of trade amount.
public static double getDefaultSecurityDepositAsPercent() {
return MIN_SECURITY_DEPOSIT_PCT;
}
public static double getMinBuyerSecurityDepositAsPercent() {
return 0.15; // 15% of trade amount.
public static double getMinSecurityDepositAsPercent() {
return MIN_SECURITY_DEPOSIT_PCT;
}
public static double getMaxBuyerSecurityDepositAsPercent() {
return 0.5; // 50% of trade amount. For a 1 BTC trade it is about 3500 USD @ 7000 USD/BTC
public static double getMaxSecurityDepositAsPercent() {
return MAX_SECURITY_DEPOSIT_PCT;
}
// We use MIN_BUYER_SECURITY_DEPOSIT as well as lower bound in case of small trade amounts.
// So 0.0005 BTC is the min. buyer security deposit even with amount of 0.0001 BTC and 0.05% percentage value.
public static BigInteger getMinBuyerSecurityDeposit() {
return MIN_BUYER_SECURITY_DEPOSIT;
}
public static double getSellerSecurityDepositAsPercent() {
return 0.15; // 15% of trade amount.
}
public static BigInteger getMinSellerSecurityDeposit() {
return MIN_SELLER_SECURITY_DEPOSIT;
public static BigInteger getMinSecurityDeposit() {
return MIN_SECURITY_DEPOSIT;
}
// This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT