limit sell offers to unsigned buy limit then warn within release windows

This commit is contained in:
woodser 2024-02-13 13:17:39 -05:00
parent a63118d5eb
commit f91f213cd2
8 changed files with 116 additions and 15 deletions

View File

@ -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
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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) {

View File

@ -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\

View File

@ -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"))

View File

@ -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;
} }

View File

@ -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();
} }
}
}); });
} }

View File

@ -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);

View File

@ -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`.