diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 8e7ee37291a..89cc9074af4 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -20,6 +20,7 @@ import bisq.core.account.sign.SignedWitness; import bisq.core.account.sign.SignedWitnessService; import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferRestrictions; @@ -77,6 +78,7 @@ public class AccountAgeWitnessService { private static final Date RELEASE = Utilities.getUTCDate(2017, GregorianCalendar.NOVEMBER, 11); public static final Date FULL_ACTIVATION = Utilities.getUTCDate(2018, GregorianCalendar.FEBRUARY, 15); + // TODO: Change to March, 1, 2019 before release private static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 1).getTime(); public enum AccountAge { @@ -86,6 +88,25 @@ public enum AccountAge { TWO_MONTHS_OR_MORE } + public enum SignState { + UNSIGNED(Res.get("offerbook.timeSinceSigning.notSigned")), + ARBITRATOR(Res.get("offerbook.timeSinceSigning.info.arbitrator")), + PEER_INITIAL(Res.get("offerbook.timeSinceSigning.info.peer")), + PEER_LIMIT_LIFTED(Res.get("offerbook.timeSinceSigning.info.peerLimitLifted")), + PEER_SIGNER(Res.get("offerbook.timeSinceSigning.info.signer")); + + private String presentation; + + SignState(String presentation) { + this.presentation = presentation; + } + + public String getPresentation() { + return presentation; + } + + } + private final KeyRing keyRing; private final P2PService p2PService; private final User user; @@ -238,6 +259,12 @@ public long getAccountAge(PaymentAccountPayload paymentAccountPayload, PubKeyRin .orElse(-1L); } + public long getAccountAge(Offer offer) { + return findWitness(offer) + .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) + .orElse(-1L); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Signed age /////////////////////////////////////////////////////////////////////////////////////////// @@ -293,12 +320,18 @@ private long getTradeLimit(Coin maxTradeLimit, String currencyCode, AccountAgeWitness accountAgeWitness, AccountAge accountAgeCategory, - OfferPayload.Direction direction) { + OfferPayload.Direction direction, + PaymentMethod paymentMethod) { if (CurrencyUtil.isFiatCurrency(currencyCode)) { double factor; - + boolean isRisky = PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode); + if (!isRisky) { + // Get age of witness rather than time since signing for non risky payment methods + accountAgeCategory = getAccountAgeCategory(getAccountAge(accountAgeWitness, new Date())); + } long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; - if (direction == OfferPayload.Direction.BUY) { + if (direction == OfferPayload.Direction.BUY && isRisky) { + // Used only for bying of BTC with risky payment methods switch (accountAgeCategory) { case TWO_MONTHS_OR_MORE: factor = 1; @@ -312,6 +345,7 @@ private long getTradeLimit(Coin maxTradeLimit, factor = 0; } } else { + // Used by non risky payment methods and for selling BTC with risky methods switch (accountAgeCategory) { case TWO_MONTHS_OR_MORE: factor = 1; @@ -347,8 +381,7 @@ private long getTradeLimit(Coin maxTradeLimit, /////////////////////////////////////////////////////////////////////////////////////////// private boolean isImmature(AccountAgeWitness accountAgeWitness) { - return accountAgeWitness.getDate() > SAFE_ACCOUNT_AGE_DATE && - getWitnessSignAge(accountAgeWitness, new Date()) < 0; + return accountAgeWitness.getDate() > SAFE_ACCOUNT_AGE_DATE; } public boolean isMyAccountAgeImmature(PaymentAccount myPaymentAccount) { @@ -393,7 +426,8 @@ public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, currencyCode, accountAgeWitness, accountAgeCategory, - direction); + direction, + paymentAccount.getPaymentMethod()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -514,7 +548,7 @@ private boolean verifyPeersTradeLimit(Offer offer, OfferPayload.Direction direction = offer.isMyOffer(keyRing) ? offer.getMirroredDirection() : offer.getDirection(); peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, peersWitness, - accountAgeCategory, direction); + accountAgeCategory, direction, offer.getPaymentMethod()); } // Makers current trade limit cannot be smaller than that in the offer boolean result = tradeAmount.value <= peersCurrentTradeLimit; @@ -651,4 +685,29 @@ public boolean accountIsSigner(AccountAgeWitness accountAgeWitness) { } return getWitnessSignAge(accountAgeWitness, new Date()) > SignedWitnessService.SIGNER_AGE; } + + public SignState getSignState(Offer offer) { + return findWitness(offer) + .map(this::getSignState) + .orElse(SignState.UNSIGNED); + } + + public SignState getSignState(AccountAgeWitness accountAgeWitness) { + if (signedWitnessService.isSignedByArbitrator(accountAgeWitness)) { + return SignState.ARBITRATOR; + } else { + final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date()); + switch (getAccountAgeCategory(accountSignAge)) { + case TWO_MONTHS_OR_MORE: + return SignState.PEER_SIGNER; + case ONE_TO_TWO_MONTHS: + return SignState.PEER_LIMIT_LIFTED; + case LESS_ONE_MONTH: + return SignState.PEER_INITIAL; + case UNVERIFIED: + default: + return SignState.UNSIGNED; + } + } + } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java index c4d5e545371..fbc4d491bf9 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -65,7 +65,7 @@ public static boolean isAmountValidForOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService) { boolean hasChargebackRisk = PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection()) > offer.getAmount().value; + offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getAmount().value; return !hasChargebackRisk || hasValidAccountAgeWitness; } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7edef9294a9..90d033a1d67 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -331,10 +331,18 @@ offerbook.availableOffers=Available offers offerbook.filterByCurrency=Filter by currency offerbook.filterByPaymentMethod=Filter by payment method offerbook.timeSinceSigning=Time since signing +offerbook.timeSinceSigning.info=This account was verified and {0} +offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peers accounts +offerbook.timeSinceSigning.info.peer=signed by a peer, waiting for limits to be lifted +offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted +offerbook.timeSinceSigning.info.signer=signed by peer and can sign peers accounts +offerbook.timeSinceSigning.daysSinceSigning={0} days offerbook.timeSinceSigning.help=By trading with a payment account that was verified by an arbitrator or a peer, your account gets signed as well.\n\ 30 days later the initial limit of 0.01 BTC gets lifted and after 90 days your account can sign other peers as well. offerbook.timeSinceSigning.notSigned=Not signed yet +offerbook.timeSinceSigning.notSigned.noNeed=Unsigned shared.notSigned=This account hasn't been signed yet. +shared.notSigned.noNeed=This account type doesn't use signing offerbook.nrOffers=No. of offers: {0} offerbook.volume={0} (min - max) diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java index 172c0296dd8..ef119ec47f7 100644 --- a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java @@ -25,6 +25,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentMethod; import bisq.core.trade.Trade; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; @@ -33,6 +34,8 @@ import com.google.common.base.Charsets; +import org.apache.commons.lang3.StringUtils; + import javafx.scene.Group; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; @@ -152,6 +155,7 @@ private PeerInfoIcon(NodeAddress nodeAddress, // outer circle Color ringColor; if (isFiatCurrency) { + switch (accountAgeWitnessService.getPeersAccountAgeCategory(peersAccountAge)) { case TWO_MONTHS_OR_MORE: ringColor = Color.rgb(0, 225, 0); // > 2 months green @@ -243,16 +247,11 @@ private PeerInfoIcon(NodeAddress nodeAddress, getChildren().addAll(outerBackground, innerBackground, avatarImageView, tagPane, numTradesPane); - //TODO sqrrm: We need these states in here: - // - signed by arbitrator - // - signed by peer - // - signed by peer and limit lifted - // - signed by peer and able to sign - // - not signing necessary for this payment account - // - signing required and not signed - // Additionally we need to have some enum or so how the account signing took place. - // e.g. if in the future we'll also offer the "pay with two different accounts"-signing - String accountSigningState = Res.get("shared.notSigned"); + boolean needsSigning = PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); + String accountSigningState = Res.get("shared.notSigned.noNeed"); + if (needsSigning) { + accountSigningState = StringUtils.capitalize(accountAgeWitnessService.getSignState(offer).getPresentation()); + } addMouseListener(numTrades, privateNotificationManager, offer, preferences, formatter, useDevPrivilegeKeys, isFiatCurrency, peersAccountAge, accountSigningState); } @@ -264,13 +263,12 @@ private long getPeersAccountAge(@Nullable Trade trade, @Nullable Offer offer) { // unexpected return -1; } - - return accountAgeWitnessService.getWitnessSignAge(trade, new Date()); - } else { - checkNotNull(offer, "Offer must not be null if trade is null."); - + } + checkNotNull(offer, "Offer must not be null if trade is null."); + if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) { return accountAgeWitnessService.getWitnessSignAge(offer, new Date()); } + return accountAgeWitnessService.getAccountAge(offer); } protected void addMouseListener(int numTrades, @@ -279,7 +277,9 @@ protected void addMouseListener(int numTrades, Preferences preferences, BSFormatter formatter, boolean useDevPrivilegeKeys, - boolean isFiatCurrency, long makersAccountAge, String accountSigningState) { + boolean isFiatCurrency, + long makersAccountAge, + String accountSigningState) { final String accountAgeTagEditor = isFiatCurrency ? makersAccountAge > -1 ? DisplayUtils.formatAccountAge(makersAccountAge) : @@ -289,7 +289,8 @@ protected void addMouseListener(int numTrades, setOnMouseClicked(e -> new PeerInfoWithTagEditor(privateNotificationManager, offer, preferences, useDevPrivilegeKeys) .fullAddress(fullAddress) .numTrades(numTrades) - .accountAge(accountAgeTagEditor).accountSigningState(accountSigningState) + .accountAge(accountAgeTagEditor) + .accountSigningState(accountSigningState) .position(localToScene(new Point2D(0, 0))) .onSave(newTag -> { preferences.setTagForPeer(fullAddress, newTag); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 7cff53410e7..f15b7ea20eb 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -107,7 +107,9 @@ import javafx.util.StringConverter; import java.util.Comparator; +import java.util.Date; import java.util.Optional; +import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; @@ -1069,25 +1071,25 @@ public void updateItem(final OfferBookListItem item, boolean empty) { String timeSinceSigning; if (accountAgeWitnessService.hasSignedWitness(item.getOffer())) { - //TODO sqrrm: We need four states in here: - // - signed by arbitrator - // - signed by peer - // - signed by peer and limit lifted - // - signed by peer and able to sign - // Additionally we need to have some enum or so how the account signing took place. - // e.g. if in the future we'll also offer the "pay with two different accounts"-signing icon = MaterialDesignIcon.APPROVAL; - info = "This account was verified and signed by an arbitrator or peer."; - //TODO sqrrm: add time since signing - timeSinceSigning = "3 days"; + info = Res.get("offerbook.timeSinceSigning.info", + accountAgeWitnessService.getSignState(item.getOffer()).getPresentation()); + long daysSinceSigning = TimeUnit.MILLISECONDS.toDays( + accountAgeWitnessService.getWitnessSignAge(item.getOffer(), new Date())); + timeSinceSigning = Res.get("offerbook.timeSinceSigning.daysSinceSigning", + daysSinceSigning); } else { - - //TODO sqrrm: Here we need two states: - // - not signing necessary for this payment account - // - signing required and not signed - icon = MaterialDesignIcon.ALERT_CIRCLE_OUTLINE; - info = Res.get("shared.notSigned"); - timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned"); + boolean needsSigning = PaymentMethod.hasChargebackRisk( + item.getOffer().getPaymentMethod(), item.getOffer().getCurrencyCode()); + if (needsSigning) { + icon = MaterialDesignIcon.ALERT_CIRCLE_OUTLINE; + info = Res.get("shared.notSigned"); + timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned"); + } else { + icon = MaterialDesignIcon.INFORMATION_OUTLINE; + info = Res.get("shared.notSigned.noNeed"); + timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned.noNeed"); + } } InfoAutoTooltipLabel label = new InfoAutoTooltipLabel(timeSinceSigning, icon, ContentDisplay.RIGHT, info);