mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
limit sell offers to unsigned buy limit then warn within release windows
This commit is contained in:
parent
a63118d5eb
commit
f91f213cd2
8 changed files with 116 additions and 15 deletions
|
@ -433,10 +433,12 @@ public class AccountAgeWitnessService {
|
||||||
limit = BigInteger.valueOf(MathUtils.roundDoubleToLong(maxTradeLimit.longValueExact() * factor));
|
limit = BigInteger.valueOf(MathUtils.roundDoubleToLong(maxTradeLimit.longValueExact() * factor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (accountAgeWitness != null) {
|
||||||
log.debug("limit={}, factor={}, accountAgeWitnessHash={}",
|
log.debug("limit={}, factor={}, accountAgeWitnessHash={}",
|
||||||
limit,
|
limit,
|
||||||
factor,
|
factor,
|
||||||
Utilities.bytesAsHexString(accountAgeWitness.getHash()));
|
Utilities.bytesAsHexString(accountAgeWitness.getHash()));
|
||||||
|
}
|
||||||
return limit;
|
return limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -518,6 +520,15 @@ public class AccountAgeWitnessService {
|
||||||
paymentAccount.getPaymentMethod()).longValueExact();
|
paymentAccount.getPaymentMethod()).longValueExact();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getUnsignedTradeLimit(PaymentMethod paymentMethod, String currencyCode, OfferDirection direction) {
|
||||||
|
return getTradeLimit(paymentMethod.getMaxTradeLimit(currencyCode),
|
||||||
|
currencyCode,
|
||||||
|
null,
|
||||||
|
AccountAge.UNVERIFIED,
|
||||||
|
direction,
|
||||||
|
paymentMethod).longValueExact();
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Verification
|
// Verification
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -44,6 +44,8 @@ import java.security.PrivateKey;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.DecimalFormatSymbols;
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -60,8 +62,13 @@ import org.bitcoinj.core.Coin;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class HavenoUtils {
|
public class HavenoUtils {
|
||||||
|
|
||||||
// Use the US locale as a base for all DecimalFormats (commas should be omitted from number strings).
|
// configurable
|
||||||
public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US);
|
private static final String RELEASE_DATE = "01-03-2024 00:00:00"; // optionally set to release date of the network in format dd-mm-yyyy to impose temporary limits, etc. e.g. "01-03-2024 00:00:00"
|
||||||
|
public static final int RELEASE_LIMIT_DAYS = 60; // number of days to limit sell offers to max buy limit for new accounts
|
||||||
|
public static final int WARN_ON_OFFER_EXCEEDS_UNSIGNED_BUY_LIMIT_DAYS = 182; // number of days to warn if sell offer exceeds unsigned buy limit
|
||||||
|
|
||||||
|
// non-configurable
|
||||||
|
public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // use the US locale as a base for all DecimalFormats (commas should be omitted from number strings)
|
||||||
public static int XMR_SMALLEST_UNIT_EXPONENT = 12;
|
public static int XMR_SMALLEST_UNIT_EXPONENT = 12;
|
||||||
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
|
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
|
||||||
public static final String LOCALHOST = "localhost";
|
public static final String LOCALHOST = "localhost";
|
||||||
|
@ -70,14 +77,34 @@ public class HavenoUtils {
|
||||||
public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS);
|
public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS);
|
||||||
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
|
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
|
||||||
|
|
||||||
// TODO: better way to share references?
|
public static ArbitrationManager arbitrationManager; // TODO: better way to share references?
|
||||||
public static ArbitrationManager arbitrationManager;
|
|
||||||
public static HavenoSetup havenoSetup;
|
public static HavenoSetup havenoSetup;
|
||||||
|
|
||||||
public static boolean isSeedNode() {
|
public static boolean isSeedNode() {
|
||||||
return havenoSetup == null;
|
return havenoSetup == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static Date getReleaseDate() {
|
||||||
|
if (RELEASE_DATE == null) return null;
|
||||||
|
try {
|
||||||
|
return DATE_FORMAT.parse(RELEASE_DATE);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to parse release date: " + RELEASE_DATE, e);
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isReleasedWithinDays(int days) {
|
||||||
|
Date releaseDate = getReleaseDate();
|
||||||
|
if (releaseDate == null) return false;
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(releaseDate);
|
||||||
|
calendar.add(Calendar.DATE, days);
|
||||||
|
Date releaseDatePlusDays = calendar.getTime();
|
||||||
|
return new Date().before(releaseDatePlusDays);
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------- CONVERSION UTILS -------------------------------
|
// ----------------------- CONVERSION UTILS -------------------------------
|
||||||
|
|
||||||
public static BigInteger coinToAtomicUnits(Coin coin) {
|
public static BigInteger coinToAtomicUnits(Coin coin) {
|
||||||
|
|
|
@ -412,6 +412,8 @@ popup.warning.tradeLimitDueAccountAgeRestriction.seller=The allowed trade amount
|
||||||
- The buyer''s account has not been signed by an arbitrator or a peer\n\
|
- The buyer''s account has not been signed by an arbitrator or a peer\n\
|
||||||
- The time since signing of the buyer''s account is not at least 30 days\n\
|
- The time since signing of the buyer''s account is not at least 30 days\n\
|
||||||
- The payment method for this offer is considered risky for bank chargebacks\n\n{1}
|
- The payment method for this offer is considered risky for bank chargebacks\n\n{1}
|
||||||
|
popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit=This payment method is temporarily limited to {0} until {1} because all buyers have new accounts.\n\n{2}
|
||||||
|
popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit=Your offer will be limited to buyers with signed and aged accounts because it exceeds {0}.\n\n{1}
|
||||||
popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\
|
popup.warning.tradeLimitDueAccountAgeRestriction.buyer=The allowed trade amount is limited to {0} because of security restrictions based on the following criteria:\n\
|
||||||
- Your account has not been signed by an arbitrator or a peer\n\
|
- Your account has not been signed by an arbitrator or a peer\n\
|
||||||
- The time since signing of your account is not at least 30 days\n\
|
- The time since signing of your account is not at least 30 days\n\
|
||||||
|
|
|
@ -380,7 +380,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler {
|
||||||
// if no warning popup has been shown yet, prompt user if they really intend to shut down
|
// if no warning popup has been shown yet, prompt user if they really intend to shut down
|
||||||
String key = "popup.info.shutDownQuery";
|
String key = "popup.info.shutDownQuery";
|
||||||
if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) {
|
if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) {
|
||||||
new Popup().headLine(Res.get("popup.info.shutDownQuery"))
|
new Popup().headLine(Res.get(key))
|
||||||
.actionButtonText(Res.get("shared.yes"))
|
.actionButtonText(Res.get("shared.yes"))
|
||||||
.onAction(() -> resp.complete(true))
|
.onAction(() -> resp.complete(true))
|
||||||
.closeButtonText(Res.get("shared.no"))
|
.closeButtonText(Res.get("shared.no"))
|
||||||
|
|
|
@ -455,6 +455,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
long getMaxTradeLimit() {
|
long getMaxTradeLimit() {
|
||||||
|
|
||||||
|
// disallow offers which no buyer can take due to trade limits on release
|
||||||
|
if (HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS)) {
|
||||||
|
return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), OfferDirection.BUY);
|
||||||
|
}
|
||||||
|
|
||||||
if (paymentAccount != null) {
|
if (paymentAccount != null) {
|
||||||
return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction);
|
return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction);
|
||||||
} else {
|
} else {
|
||||||
|
@ -586,6 +592,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||||
// Getters
|
// Getters
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public BigInteger getMaxUnsignedBuyLimit() {
|
||||||
|
return BigInteger.valueOf(accountAgeWitnessService.getUnsignedTradeLimit(paymentAccount.getPaymentMethod(), tradeCurrencyCode.get(), OfferDirection.BUY));
|
||||||
|
}
|
||||||
|
|
||||||
protected ReadOnlyObjectProperty<BigInteger> getAmount() {
|
protected ReadOnlyObjectProperty<BigInteger> getAmount() {
|
||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1012,8 +1012,25 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||||
|
|
||||||
nextButton.setOnAction(e -> {
|
nextButton.setOnAction(e -> {
|
||||||
if (model.isPriceInRange()) {
|
if (model.isPriceInRange()) {
|
||||||
|
|
||||||
|
// warn if sell offer exceeds unsigned buy limit within release window
|
||||||
|
boolean isSellOffer = model.getDataModel().isSellOffer();
|
||||||
|
boolean exceedsUnsignedBuyLimit = model.getDataModel().getAmount().get().compareTo(model.getDataModel().getMaxUnsignedBuyLimit()) > 0;
|
||||||
|
String key = "popup.warning.tradeLimitDueAccountAgeRestriction.seller.exceedsUnsignedBuyLimit";
|
||||||
|
if (isSellOffer && exceedsUnsignedBuyLimit && DontShowAgainLookup.showAgain(key) && HavenoUtils.isReleasedWithinDays(HavenoUtils.WARN_ON_OFFER_EXCEEDS_UNSIGNED_BUY_LIMIT_DAYS)) {
|
||||||
|
new Popup().information(Res.get(key,
|
||||||
|
HavenoUtils.formatXmr(model.getDataModel().getMaxUnsignedBuyLimit(), true),
|
||||||
|
Res.get("offerbook.warning.newVersionAnnouncement")))
|
||||||
|
.closeButtonText(Res.get("shared.cancel"))
|
||||||
|
.actionButtonText(Res.get("shared.ok"))
|
||||||
|
.onAction(this::onShowPayFundsScreen)
|
||||||
|
.width(900)
|
||||||
|
.dontShowAgainId(key)
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
onShowPayFundsScreen();
|
onShowPayFundsScreen();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,9 @@ import org.bitcoinj.core.Coin;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static javafx.beans.binding.Bindings.createStringBinding;
|
import static javafx.beans.binding.Bindings.createStringBinding;
|
||||||
|
@ -692,6 +695,26 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||||
} else {
|
} else {
|
||||||
amount.set(HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit()));
|
amount.set(HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit()));
|
||||||
boolean isBuy = dataModel.getDirection() == OfferDirection.BUY;
|
boolean isBuy = dataModel.getDirection() == OfferDirection.BUY;
|
||||||
|
boolean isSellerWithinReleaseWindow = !isBuy && HavenoUtils.isReleasedWithinDays(HavenoUtils.RELEASE_LIMIT_DAYS);
|
||||||
|
if (isSellerWithinReleaseWindow) {
|
||||||
|
|
||||||
|
// format release date plus days
|
||||||
|
Date releaseDate = HavenoUtils.getReleaseDate();
|
||||||
|
Calendar c = Calendar.getInstance();
|
||||||
|
c.setTime(releaseDate);
|
||||||
|
c.add(Calendar.DATE, HavenoUtils.RELEASE_LIMIT_DAYS);
|
||||||
|
Date releaseDatePlusDays = c.getTime();
|
||||||
|
SimpleDateFormat formatter = new SimpleDateFormat("MMMM d, yyyy");
|
||||||
|
String releaseDatePlusDaysAsString = formatter.format(releaseDatePlusDays);
|
||||||
|
|
||||||
|
// popup temporary restriction
|
||||||
|
new Popup().information(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.seller.releaseLimit",
|
||||||
|
HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true),
|
||||||
|
releaseDatePlusDaysAsString,
|
||||||
|
Res.get("offerbook.warning.newVersionAnnouncement")))
|
||||||
|
.width(900)
|
||||||
|
.show();
|
||||||
|
} else {
|
||||||
new Popup().information(Res.get(isBuy ? "popup.warning.tradeLimitDueAccountAgeRestriction.buyer" : "popup.warning.tradeLimitDueAccountAgeRestriction.seller",
|
new Popup().information(Res.get(isBuy ? "popup.warning.tradeLimitDueAccountAgeRestriction.buyer" : "popup.warning.tradeLimitDueAccountAgeRestriction.seller",
|
||||||
HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true),
|
HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true),
|
||||||
Res.get("offerbook.warning.newVersionAnnouncement")))
|
Res.get("offerbook.warning.newVersionAnnouncement")))
|
||||||
|
@ -699,6 +722,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// We want to trigger a recalculation of the volume
|
// We want to trigger a recalculation of the volume
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
onFocusOutVolumeTextField(true, false);
|
onFocusOutVolumeTextField(true, false);
|
||||||
|
|
|
@ -78,6 +78,16 @@ Keypairs with alert privileges are able to send alerts, e.g. to update the appli
|
||||||
|
|
||||||
Set the XMR address to collect trade fees in `getTradeFeeAddress()` in HavenoUtils.java.
|
Set the XMR address to collect trade fees in `getTradeFeeAddress()` in HavenoUtils.java.
|
||||||
|
|
||||||
|
## Set the network's release date
|
||||||
|
|
||||||
|
Optionally set the network's approximate release date by setting `RELEASE_DATE` in HavenoUtils.java.
|
||||||
|
|
||||||
|
This will prevent posting sell offers which no buyers can take before any buyer accounts are signed and aged, while the network bootstraps.
|
||||||
|
|
||||||
|
After a period (default 60 days), the limit is lifted and sellers can post offers exceeding unsigned buy limits, but they will receive an informational warning for an additional period (default 6 months after release).
|
||||||
|
|
||||||
|
The defaults can be adjusted with the related constants in HavenoUtils.java.
|
||||||
|
|
||||||
## Create and register arbitrators
|
## Create and register arbitrators
|
||||||
|
|
||||||
Before running the arbitrator, remember that at least one seednode should already be deployed and its address listed in `core/src/main/resources/xmr_<network>.seednodes`.
|
Before running the arbitrator, remember that at least one seednode should already be deployed and its address listed in `core/src/main/resources/xmr_<network>.seednodes`.
|
||||||
|
|
Loading…
Reference in a new issue