diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java index c8724b0f4c..ffdf3f3afa 100644 --- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java @@ -433,10 +433,12 @@ public class AccountAgeWitnessService { limit = BigInteger.valueOf(MathUtils.roundDoubleToLong(maxTradeLimit.longValueExact() * factor)); } - log.debug("limit={}, factor={}, accountAgeWitnessHash={}", - limit, - factor, - Utilities.bytesAsHexString(accountAgeWitness.getHash())); + if (accountAgeWitness != null) { + log.debug("limit={}, factor={}, accountAgeWitnessHash={}", + limit, + factor, + Utilities.bytesAsHexString(accountAgeWitness.getHash())); + } return limit; } @@ -518,6 +520,15 @@ public class AccountAgeWitnessService { 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 /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 1b2fc24ac0..dec84f5597 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -44,6 +44,8 @@ import java.security.PrivateKey; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; import java.util.Locale; import java.util.concurrent.CountDownLatch; import javax.annotation.Nullable; @@ -60,8 +62,13 @@ import org.bitcoinj.core.Coin; @Slf4j public class HavenoUtils { - // Use the US locale as a base for all DecimalFormats (commas should be omitted from number strings). - public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); + // configurable + 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 final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node 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 SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); - // TODO: better way to share references? - public static ArbitrationManager arbitrationManager; + public static ArbitrationManager arbitrationManager; // TODO: better way to share references? public static HavenoSetup havenoSetup; public static boolean isSeedNode() { 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 ------------------------------- public static BigInteger coinToAtomicUnits(Coin coin) { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index b41b5b35a9..37602491d4 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -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 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} +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\ - 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\ diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java index 553f78c21e..0b3ebb42b7 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java @@ -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 String key = "popup.info.shutDownQuery"; 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")) .onAction(() -> resp.complete(true)) .closeButtonText(Res.get("shared.no")) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index e81c13b2c0..c29ffa67db 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -455,6 +455,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } 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) { return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction); } else { @@ -586,6 +592,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel { // Getters /////////////////////////////////////////////////////////////////////////////////////////// + public BigInteger getMaxUnsignedBuyLimit() { + return BigInteger.valueOf(accountAgeWitnessService.getUnsignedTradeLimit(paymentAccount.getPaymentMethod(), tradeCurrencyCode.get(), OfferDirection.BUY)); + } + protected ReadOnlyObjectProperty getAmount() { return amount; } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 25a95fcad2..bceeaf6ffe 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -1012,7 +1012,24 @@ public abstract class MutableOfferView> exten nextButton.setOnAction(e -> { if (model.isPriceInRange()) { - onShowPayFundsScreen(); + + // 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(); + } } }); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 4d41777a24..5a5c0eb23e 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -81,6 +81,9 @@ import org.bitcoinj.core.Coin; import javax.inject.Inject; import javax.inject.Named; import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; import java.util.concurrent.TimeUnit; import static javafx.beans.binding.Bindings.createStringBinding; @@ -692,11 +695,32 @@ public abstract class MutableOfferViewModel ext } else { amount.set(HavenoUtils.formatXmr(xmrValidator.getMaxTradeLimit())); boolean isBuy = dataModel.getDirection() == OfferDirection.BUY; - new Popup().information(Res.get(isBuy ? "popup.warning.tradeLimitDueAccountAgeRestriction.buyer" : "popup.warning.tradeLimitDueAccountAgeRestriction.seller", - HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true), - Res.get("offerbook.warning.newVersionAnnouncement"))) - .width(900) - .show(); + 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", + HavenoUtils.formatXmr(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT, true), + Res.get("offerbook.warning.newVersionAnnouncement"))) + .width(900) + .show(); + } } } // We want to trigger a recalculation of the volume diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index b95708bd33..45979e865d 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -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 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 Before running the arbitrator, remember that at least one seednode should already be deployed and its address listed in `core/src/main/resources/xmr_.seednodes`.