diff --git a/PreferencesPayload b/PreferencesPayload new file mode 100644 index 00000000000..97d0f2b1949 Binary files /dev/null and b/PreferencesPayload differ diff --git a/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java b/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java deleted file mode 100644 index b5b50a8ad14..00000000000 --- a/assets/src/main/java/bisq/asset/coins/TetherUSDLiquid.java +++ /dev/null @@ -1,12 +0,0 @@ -package bisq.asset.coins; - -import bisq.asset.Coin; -import bisq.asset.LiquidBitcoinAddressValidator; - -public class TetherUSDLiquid extends Coin { - public TetherUSDLiquid() { - // If you add a new USDT variant or want to change this ticker symbol you should also look here: - // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (Liquid Bitcoin)", "L-USDT", new LiquidBitcoinAddressValidator()); - } -} diff --git a/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java b/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java deleted file mode 100644 index b0ab624d713..00000000000 --- a/assets/src/main/java/bisq/asset/coins/TetherUSDOmni.java +++ /dev/null @@ -1,12 +0,0 @@ -package bisq.asset.coins; - -import bisq.asset.Base58BitcoinAddressValidator; -import bisq.asset.Coin; - -public class TetherUSDOmni extends Coin { - public TetherUSDOmni() { - // If you add a new USDT variant or want to change this ticker symbol you should also look here: - // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (Omni)", "USDT-O", new Base58BitcoinAddressValidator()); - } -} diff --git a/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java deleted file mode 100644 index cb57361a1f4..00000000000 --- a/assets/src/main/java/bisq/asset/tokens/TetherUSDERC20.java +++ /dev/null @@ -1,11 +0,0 @@ -package bisq.asset.tokens; - -import bisq.asset.Erc20Token; - -public class TetherUSDERC20 extends Erc20Token { - public TetherUSDERC20() { - // If you add a new USDT variant or want to change this ticker symbol you should also look here: - // core/src/main/java/bisq/core/provider/price/PriceProvider.java:getAll() - super("Tether USD (ERC20)", "USDT-E"); - } -} diff --git a/assets/src/main/resources/META-INF/services/bisq.asset.Asset b/assets/src/main/resources/META-INF/services/bisq.asset.Asset index a2f77816d66..80a6168463b 100644 --- a/assets/src/main/resources/META-INF/services/bisq.asset.Asset +++ b/assets/src/main/resources/META-INF/services/bisq.asset.Asset @@ -104,8 +104,6 @@ bisq.asset.coins.Spectrecoin bisq.asset.coins.Starwels bisq.asset.coins.SUB1X bisq.asset.coins.TEO -bisq.asset.coins.TetherUSDLiquid -bisq.asset.coins.TetherUSDOmni bisq.asset.coins.TurtleCoin bisq.asset.coins.UnitedCommunityCoin bisq.asset.coins.Unobtanium @@ -125,7 +123,6 @@ bisq.asset.coins.ZeroClassic bisq.asset.tokens.AugmintEuro bisq.asset.tokens.DaiStablecoin bisq.asset.tokens.EtherStone -bisq.asset.tokens.TetherUSDERC20 bisq.asset.tokens.TrueUSD bisq.asset.tokens.USDCoin bisq.asset.tokens.VectorspaceAI diff --git a/build.gradle b/build.gradle index c82ff3336ad..932644c115d 100644 --- a/build.gradle +++ b/build.gradle @@ -376,7 +376,7 @@ configure(project(':desktop')) { apply plugin: 'witness' apply from: '../gradle/witness/gradle-witness.gradle' - version = '1.3.7' + version = '1.3.8' mainClassName = 'bisq.desktop.app.BisqAppMain' diff --git a/common/src/main/java/bisq/common/app/Version.java b/common/src/main/java/bisq/common/app/Version.java index 926c361e4f4..986a6ed59b7 100644 --- a/common/src/main/java/bisq/common/app/Version.java +++ b/common/src/main/java/bisq/common/app/Version.java @@ -17,6 +17,9 @@ package bisq.common.app; +import java.util.Arrays; +import java.util.List; + import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; @@ -27,7 +30,18 @@ public class Version { // VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update // Therefore all sub versions start again with 1 // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.3.7"; + public static final String VERSION = "1.3.8"; + + /** + * Holds a list of historical versions. + * + * Has been introduced with version 1.3.2. Prior versions did not need the history. + * + * The history of versions is used in the differential data store solution which + * was introduced to eliminate the need to rewrite all data to disk every time the + * data changes. + */ + public static final List history = Arrays.asList("1.3.5", "1.3.6", "1.3.7"); public static int getMajorVersion(String version) { return getSubVersion(version, 0); diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java index 722522cba78..8476efee836 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessStorageService.java @@ -20,6 +20,7 @@ import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.payload.PersistableNetworkPayload; import bisq.network.p2p.storage.persistence.MapStoreService; +import bisq.network.p2p.storage.persistence.SplitStoreService; import bisq.common.config.Config; import bisq.common.storage.Storage; @@ -36,7 +37,7 @@ import static com.google.common.base.Preconditions.checkArgument; @Slf4j -public class SignedWitnessStorageService extends MapStoreService { +public class SignedWitnessStorageService extends SplitStoreService { private static final String FILE_NAME = "SignedWitnessStore"; @@ -59,11 +60,6 @@ public String getFileName() { return FILE_NAME; } - @Override - public Map getMap() { - return store.getMap(); - } - @Override public boolean canHandle(PersistableNetworkPayload payload) { return payload instanceof SignedWitness; @@ -78,12 +74,4 @@ public boolean canHandle(PersistableNetworkPayload payload) { protected SignedWitnessStore createStore() { return new SignedWitnessStore(); } - - @Override - protected void readStore() { - super.readStore(); - checkArgument(store instanceof SignedWitnessStore, - "Store is not instance of SignedWitnessStore. That can happen if the ProtoBuffer " + - "file got changed. We cleared the data store and recreated it again."); - } } diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java index 8b0e340c41f..d8fae0cbc84 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessStore.java @@ -19,18 +19,13 @@ import bisq.network.p2p.storage.P2PDataStorage; -import bisq.network.p2p.storage.payload.PersistableNetworkPayload; - -import bisq.common.proto.persistable.ThreadedPersistableEnvelope; +import bisq.network.p2p.storage.persistence.SplitStore; import com.google.protobuf.Message; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -40,10 +35,7 @@ * definition and provide a hashMap for the domain access. */ @Slf4j -public class SignedWitnessStore implements ThreadedPersistableEnvelope { - @Getter - private Map map = new ConcurrentHashMap<>(); - +public class SignedWitnessStore extends SplitStore { SignedWitnessStore() { } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java index bc261e7efe5..625778290b5 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStorageService.java @@ -17,9 +17,8 @@ package bisq.core.account.witness; -import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.payload.PersistableNetworkPayload; -import bisq.network.p2p.storage.persistence.MapStoreService; +import bisq.network.p2p.storage.persistence.SplitStoreService; import bisq.common.config.Config; import bisq.common.storage.Storage; @@ -29,14 +28,10 @@ import java.io.File; -import java.util.Map; - import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; - @Slf4j -public class AccountAgeWitnessStorageService extends MapStoreService { +public class AccountAgeWitnessStorageService extends SplitStoreService { private static final String FILE_NAME = "AccountAgeWitnessStore"; @@ -59,11 +54,6 @@ public String getFileName() { return FILE_NAME; } - @Override - public Map getMap() { - return store.getMap(); - } - @Override public boolean canHandle(PersistableNetworkPayload payload) { return payload instanceof AccountAgeWitness; @@ -78,12 +68,4 @@ public boolean canHandle(PersistableNetworkPayload payload) { protected AccountAgeWitnessStore createStore() { return new AccountAgeWitnessStore(); } - - @Override - protected void readStore() { - super.readStore(); - checkArgument(store instanceof AccountAgeWitnessStore, - "Store is not instance of AccountAgeWitnessStore. That can happen if the ProtoBuffer " + - "file got changed. We cleared the data store and recreated it again."); - } } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java index 4731cb9e624..5321ce959b0 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessStore.java @@ -18,18 +18,13 @@ package bisq.core.account.witness; import bisq.network.p2p.storage.P2PDataStorage; -import bisq.network.p2p.storage.payload.PersistableNetworkPayload; - -import bisq.common.proto.persistable.ThreadedPersistableEnvelope; +import bisq.network.p2p.storage.persistence.SplitStore; import com.google.protobuf.Message; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -39,10 +34,7 @@ * definition and provide a hashMap for the domain access. */ @Slf4j -public class AccountAgeWitnessStore implements ThreadedPersistableEnvelope { - @Getter - private Map map = new ConcurrentHashMap<>(); - +public class AccountAgeWitnessStore extends SplitStore { AccountAgeWitnessStore() { } diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index 2cb4760f386..bb36e1ffe49 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -26,6 +26,7 @@ import bisq.core.setup.CoreSetup; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.trade.TradeManager; +import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.network.p2p.P2PService; @@ -164,7 +165,6 @@ protected void applyInjector() { protected void setupDevEnv() { DevEnv.setDevMode(config.useDevMode); - DevEnv.setDevMode(config.useDevModeHeader); DevEnv.setDaoActivated(config.daoActivated); } @@ -221,6 +221,7 @@ public void gracefulShutDown(ResultHandler resultHandler) { try { injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeManager.class).shutDown(); + injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(DaoSetup.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); injector.getInstance(OpenOfferManager.class).shutDown(() -> { diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 9440a98559b..164f74a04da 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -59,6 +59,7 @@ import bisq.core.trade.TradeManager; import bisq.core.trade.TradeTxException; import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.core.user.Preferences; import bisq.core.user.User; import bisq.core.util.FormattingUtils; @@ -113,6 +114,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import ch.qos.logback.classic.Level; @@ -167,6 +169,7 @@ default void onRequestWalletPassword() { private final PrivateNotificationManager privateNotificationManager; private final FilterManager filterManager; private final TradeStatisticsManager tradeStatisticsManager; + private final XmrTxProofService xmrTxProofService; private final ClockWatcher clockWatcher; private final FeeService feeService; private final DaoSetup daoSetup; @@ -263,6 +266,7 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, PrivateNotificationManager privateNotificationManager, FilterManager filterManager, TradeStatisticsManager tradeStatisticsManager, + XmrTxProofService xmrTxProofService, ClockWatcher clockWatcher, FeeService feeService, DaoSetup daoSetup, @@ -308,6 +312,7 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, this.privateNotificationManager = privateNotificationManager; this.filterManager = filterManager; this.tradeStatisticsManager = tradeStatisticsManager; + this.xmrTxProofService = xmrTxProofService; this.clockWatcher = clockWatcher; this.feeService = feeService; this.daoSetup = daoSetup; @@ -686,6 +691,7 @@ private void initDomainServices() { traderChatManager.onAllServicesInitialized(); tradeManager.onAllServicesInitialized(); + xmrTxProofService.onAllServicesInitialized(); if (walletsSetup.downloadPercentageProperty().get() == 1) { checkForLockedUpFunds(); @@ -846,7 +852,13 @@ private void initDomainServices() { priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); - user.onAllServicesInitialized(revolutAccountsUpdateHandler); + if (revolutAccountsUpdateHandler != null) { + revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() + .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) + .map(paymentAccount -> (RevolutAccount) paymentAccount) + .filter(RevolutAccount::userNameNotSet) + .collect(Collectors.toList())); + } allBasicServicesInitialized = true; } diff --git a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java index 1c2b9390ffb..f6c4d97e9d1 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java @@ -73,10 +73,20 @@ public void onAllServicesInitialized() { } }); mediationManager.getDisputesAsObservableList().forEach(this::setDisputeListener); + + // We do not need a handling for unread messages as mailbox messages arrive later and will trigger the + // event listeners. But the existing messages are not causing a notification. + } + + public static MobileMessage getTestMsg() { + String shortId = UUID.randomUUID().toString().substring(0, 8); + return new MobileMessage(Res.get("account.notifications.dispute.message.title"), + Res.get("account.notifications.dispute.message.msg", shortId), + shortId, + MobileMessageType.DISPUTE); } private void setDisputeListener(Dispute dispute) { - //TODO use weak ref or remove listener log.debug("We got a dispute added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); dispute.getChatMessages().addListener((ListChangeListener) c -> { log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); @@ -85,31 +95,24 @@ private void setDisputeListener(Dispute dispute) { c.getAddedSubList().forEach(chatMessage -> onChatMessage(chatMessage, dispute)); } }); - - //TODO test - if (!dispute.getChatMessages().isEmpty()) - onChatMessage(dispute.getChatMessages().get(0), dispute); } private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { - // TODO we need to prevent to send msg for old dispute messages again at restart - // Maybe we need a new property in ChatMessage - // As key is not set in initial iterations it seems we don't need an extra handling. - // the mailbox msg is set a bit later so that triggers a notification, but not the old messages. + if (chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) { + return; + } // We only send msg in case we are not the sender - if (!chatMessage.getSenderNodeAddress().equals(p2PService.getAddress())) { - String shortId = chatMessage.getShortId(); - MobileMessage message = new MobileMessage(Res.get("account.notifications.dispute.message.title"), - Res.get("account.notifications.dispute.message.msg", shortId), - shortId, - MobileMessageType.DISPUTE); - try { - mobileNotificationService.sendMessage(message); - } catch (Exception e) { - log.error(e.toString()); - e.printStackTrace(); - } + String shortId = chatMessage.getShortId(); + MobileMessage message = new MobileMessage(Res.get("account.notifications.dispute.message.title"), + Res.get("account.notifications.dispute.message.msg", shortId), + shortId, + MobileMessageType.DISPUTE); + try { + mobileNotificationService.sendMessage(message); + } catch (Exception e) { + log.error(e.toString()); + e.printStackTrace(); } // We check at every new message if it might be a message sent after the dispute had been closed. If that is the @@ -122,12 +125,4 @@ private void onChatMessage(ChatMessage chatMessage, Dispute dispute) { dispute.setIsClosed(false); } } - - public static MobileMessage getTestMsg() { - String shortId = UUID.randomUUID().toString().substring(0, 8); - return new MobileMessage(Res.get("account.notifications.dispute.message.title"), - Res.get("account.notifications.dispute.message.msg", shortId), - shortId, - MobileMessageType.DISPUTE); - } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java index f63ad2ca8ff..12a9565b710 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -173,4 +173,9 @@ public String getSaltAsHex() { public String getOwnerId() { return paymentAccountPayload.getOwnerId(); } + + public void onAddToUser() { + // We are in the process to get added to the user. This is called just before saving the account and the + // last moment we could apply some special handling if needed (e.g. as it happens for Revolut) + } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java index d4fe6951515..08a96db151b 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -20,7 +20,9 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.user.User; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -136,6 +138,13 @@ public static boolean isCryptoCurrencyAccount(PaymentAccount paymentAccount) { paymentAccount != null && paymentAccount.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT)); } + public static Optional findPaymentAccount(PaymentAccountPayload paymentAccountPayload, + User user) { + return user.getPaymentAccountsAsObservable().stream(). + filter(e -> e.getPaymentAccountPayload().equals(paymentAccountPayload)) + .findAny(); + } + // TODO no code duplication found in UI code (added for API) // That is optional and set to null if not supported (AltCoins,...) /* public static String getCountryCode(PaymentAccount paymentAccount) { diff --git a/core/src/main/java/bisq/core/payment/RevolutAccount.java b/core/src/main/java/bisq/core/payment/RevolutAccount.java index 07282769f69..93191bbac01 100644 --- a/core/src/main/java/bisq/core/payment/RevolutAccount.java +++ b/core/src/main/java/bisq/core/payment/RevolutAccount.java @@ -37,14 +37,34 @@ protected PaymentAccountPayload createPayload() { } public void setUserName(String userName) { - ((RevolutAccountPayload) paymentAccountPayload).setUserName(userName); + revolutAccountPayload().setUserName(userName); } public String getUserName() { - return ((RevolutAccountPayload) paymentAccountPayload).getUserName(); + return (revolutAccountPayload()).getUserName(); + } + + public String getAccountId() { + return (revolutAccountPayload()).getAccountId(); } public boolean userNameNotSet() { - return ((RevolutAccountPayload) paymentAccountPayload).userNameNotSet(); + return (revolutAccountPayload()).userNameNotSet(); + } + + public boolean hasOldAccountId() { + return (revolutAccountPayload()).hasOldAccountId(); + } + + private RevolutAccountPayload revolutAccountPayload() { + return (RevolutAccountPayload) paymentAccountPayload; + } + + @Override + public void onAddToUser() { + super.onAddToUser(); + + // At save we apply the userName to accountId in case it is empty for backward compatibility + revolutAccountPayload().maybeApplyUserNameToAccountId(); } } diff --git a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java index 4132b1934a8..3abfa8b2aea 100644 --- a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java @@ -39,7 +39,7 @@ @Getter @ToString @Slf4j -public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload { +public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { protected String holderName = ""; @Nullable protected String bankName; diff --git a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java index 10c39c0023a..afa97764a9d 100644 --- a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java @@ -42,7 +42,7 @@ @Setter @Getter @Slf4j -public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload { +public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { private String holderName = ""; @Nullable private String holderEmail; diff --git a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java index b8d93695e74..a320c36679e 100644 --- a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload { +public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String email = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java index 139da088893..f437d5d6720 100644 --- a/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class ClearXchangeAccountPayload extends PaymentAccountPayload { +public final class ClearXchangeAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String emailOrMobileNr = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java index f404766ad29..88b2042af87 100644 --- a/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public final class InteracETransferAccountPayload extends PaymentAccountPayload { +public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String email = ""; private String holderName = ""; private String question = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java index 4cfd4260e6f..807915b2bd8 100644 --- a/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class JapanBankAccountPayload extends PaymentAccountPayload { +public final class JapanBankAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { // bank private String bankName = ""; private String bankCode = ""; @@ -137,4 +137,9 @@ public byte[] getAgeWitnessInputData() { String all = this.bankName + this.bankBranchName + this.bankAccountType + this.bankAccountNumber + this.bankAccountName; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } + + @Override + public String getHolderName() { + return bankAccountName; + } } diff --git a/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java index e5f4ffc74d7..03734c68f68 100644 --- a/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public class MoneyGramAccountPayload extends PaymentAccountPayload { +public class MoneyGramAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String holderName; private String countryCode = ""; private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. diff --git a/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java b/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java new file mode 100644 index 00000000000..25efe937f4a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java @@ -0,0 +1,22 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.payment.payload; + +public interface PayloadWithHolderName { + String getHolderName(); +} diff --git a/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java index be018ffb558..3a451bda61c 100644 --- a/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class PopmoneyAccountPayload extends PaymentAccountPayload { +public final class PopmoneyAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String accountId = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java index 7c8926ab105..19050325cfa 100644 --- a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java @@ -19,7 +19,8 @@ import bisq.core.locale.Res; -import bisq.common.proto.ProtoUtil; +import bisq.common.util.JsonExclude; +import bisq.common.util.Tuple2; import com.google.protobuf.Message; @@ -27,19 +28,23 @@ import java.util.HashMap; import java.util.Map; -import java.util.Optional; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkArgument; + @EqualsAndHashCode(callSuper = true) @ToString @Slf4j public final class RevolutAccountPayload extends PaymentAccountPayload { - // Not used anymore from outside. Only used as internal Id to not break existing account witness objects + // Only used as internal Id to not break existing account witness objects + // We still show it in case it is different to the userName for additional security + @Getter private String accountId = ""; // Was added in 1.3.8 @@ -47,8 +52,13 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { // Old accounts get a popup to add the new required field userName but accountId is // left unchanged. Newly created accounts fill accountId with the value of userName. // In the UI we only use userName. - @Nullable - private String userName = null; + + // For backward compatibility we need to exclude the new field for the contract json. + // We can remove that after a while when risk that users with pre 1.3.8 version trade with updated + // users is very low. + @JsonExclude + @Getter + private String userName = ""; public RevolutAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -77,8 +87,8 @@ private RevolutAccountPayload(String paymentMethod, @Override public Message toProtoMessage() { protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder() - .setAccountId(accountId); - Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName); + .setAccountId(accountId) + .setUserName(userName); return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); } @@ -88,7 +98,7 @@ public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload pro return new RevolutAccountPayload(proto.getPaymentMethodId(), proto.getId(), revolutAccountPayload.getAccountId(), - ProtoUtil.stringOrNullFromProto(revolutAccountPayload.getUserName()), + revolutAccountPayload.getUserName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -100,7 +110,34 @@ public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload pro @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + getUserName(); + Tuple2 tuple = getLabelValueTuple(); + return Res.get(paymentMethodId) + " - " + tuple.first + ": " + tuple.second; + } + + private Tuple2 getLabelValueTuple() { + String label; + String value; + checkArgument(!userName.isEmpty() || hasOldAccountId(), + "Either username must be set or we have an old account with accountId"); + if (!userName.isEmpty()) { + label = Res.get("payment.account.userName"); + value = userName; + + if (hasOldAccountId()) { + label += "/" + Res.get("payment.account.phoneNr"); + value += "/" + accountId; + } + } else { + label = Res.get("payment.account.phoneNr"); + value = accountId; + } + return new Tuple2<>(label, value); + } + + public Tuple2 getRecipientsAccountData() { + Tuple2 tuple = getLabelValueTuple(); + String label = Res.get("portfolio.pending.step2_buyer.recipientsAccountData", tuple.first); + return new Tuple2<>(label, tuple.second); } @Override @@ -111,23 +148,34 @@ public String getPaymentDetailsForTradePopup() { @Override public byte[] getAgeWitnessInputData() { // getAgeWitnessInputData is called at new account creation when accountId is empty string. - return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + if (hasOldAccountId()) { + // If the accountId was already in place (updated user who had used accountId for account age) we keep the + // old accountId to not invalidate the existing account age witness. + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + + } else { + // If a new account was registered from version 1.3.8 or later we use the userName. + return super.getAgeWitnessInputData(userName.getBytes(StandardCharsets.UTF_8)); + } } - public void setUserName(@Nullable String userName) { - this.userName = userName; - // We only set accountId to userName for new accounts. Existing accounts have accountId set with email - // or phone nr. and we keep that to not break account signing. - if (accountId.isEmpty()) { - accountId = userName; - } + public boolean userNameNotSet() { + return userName.isEmpty(); } - public String getUserName() { - return userName != null ? userName : accountId; + public boolean hasOldAccountId() { + return !accountId.equals(userName); } - public boolean userNameNotSet() { - return userName == null; + public void setUserName(String userName) { + this.userName = userName; + } + + // In case it is a new account we need to fill the accountId field to support not-updated traders who are not + // aware of the new userName field + public void maybeApplyUserNameToAccountId() { + if (accountId.isEmpty()) { + accountId = userName; + } } } diff --git a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java index c10dacb8437..59425e1bcfe 100644 --- a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java @@ -43,7 +43,7 @@ @ToString @Getter @Slf4j -public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload { +public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { @Setter private String holderName = ""; @Setter @@ -158,6 +158,7 @@ public byte[] getAgeWitnessInputData() { // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(ArrayUtils.addAll(iban.getBytes(StandardCharsets.UTF_8), bic.getBytes(StandardCharsets.UTF_8))); } + @Override public String getOwnerId() { return holderName; diff --git a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java index cb8b69cd6f8..54cffcd78d2 100644 --- a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java @@ -43,7 +43,7 @@ @ToString @Getter @Slf4j -public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload { +public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { @Setter private String holderName = ""; @Setter diff --git a/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java index 6c236ba1304..f14eafb92e4 100644 --- a/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class SwishAccountPayload extends PaymentAccountPayload { +public final class SwishAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String mobileNr = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java index e705e032dc8..96a8dec5203 100644 --- a/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload { +public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String postalAddress = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java index f99b80e81f0..45f33186cc3 100644 --- a/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public class WesternUnionAccountPayload extends CountryBasedPaymentAccountPayload { +public class WesternUnionAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { private String holderName; private String city; private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. diff --git a/core/src/main/java/bisq/core/provider/price/PriceProvider.java b/core/src/main/java/bisq/core/provider/price/PriceProvider.java index bc29ecc3828..d0ac27c2d72 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceProvider.java +++ b/core/src/main/java/bisq/core/provider/price/PriceProvider.java @@ -70,24 +70,7 @@ public Tuple2, Map> getAll() throws IOExc final double price = (Double) treeMap.get("price"); // json uses double for our timestampSec long value... final long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); - - // We do not have support for the case of multiChain assets where a common price ticker used for - // different flavours of the asset. It would be quite a bit of effort to add generic support to the - // asset and tradeCurrency classes and to handle it correctly from their many client classes. - // So we decided to hack in the sub-assets as copies of the price and accept the annoyance to see - // 3 different prices for the same master asset. But who knows, maybe prices will differ over time for - // the sub assets so then we are better prepared that way... - if (currencyCode.equals("USDT")) { - addPrice(marketPriceMap, "USDT-O", price, timestampSec); - addPrice(marketPriceMap, "USDT-E", price, timestampSec); - addPrice(marketPriceMap, "L-USDT", price, timestampSec); - } else { - // NON_EXISTING_SYMBOL is returned from service for nto found items - // Sometimes it has post fixes as well so we use a 'contains' check. - if (!currencyCode.contains("NON_EXISTING_SYMBOL")) { - addPrice(marketPriceMap, currencyCode, price, timestampSec); - } - } + marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); @@ -97,13 +80,6 @@ public Tuple2, Map> getAll() throws IOExc return new Tuple2<>(tsMap, marketPriceMap); } - private void addPrice(Map marketPriceMap, - String currencyCode, - double price, - long timestampSec) { - marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); - } - public String getBaseUrl() { return httpClient.getBaseUrl(); } diff --git a/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java new file mode 100644 index 00000000000..48025ae2669 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java @@ -0,0 +1,270 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.support.dispute.agent; + +import bisq.core.locale.Res; +import bisq.core.payment.payload.PayloadWithHolderName; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.trade.Contract; +import bisq.core.user.DontShowAgainLookup; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import javafx.collections.ListChangeListener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Detects traders who had disputes where they used different account holder names. Only payment methods where a + * real name is required are used for the check. + * Strings are not translated here as it is only visible to dispute agents + */ +@Slf4j +public class MultipleHolderNameDetection { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onSuspiciousDisputeDetected(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + private static final String ACK_KEY = "Ack-"; + + private static String getSigPuKeyHashAsHex(PubKeyRing pubKeyRing) { + return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.getSignaturePubKeyBytes())); + } + + private static String getSigPubKeyHashAsHex(Dispute dispute) { + return getSigPuKeyHashAsHex(dispute.getTraderPubKeyRing()); + } + + private static boolean isBuyer(Dispute dispute) { + String traderSigPubKeyHashAsHex = getSigPubKeyHashAsHex(dispute); + String buyerSigPubKeyHashAsHex = getSigPuKeyHashAsHex(dispute.getContract().getBuyerPubKeyRing()); + return buyerSigPubKeyHashAsHex.equals(traderSigPubKeyHashAsHex); + } + + private static PayloadWithHolderName getPayloadWithHolderName(Dispute dispute) { + return (PayloadWithHolderName) getPaymentAccountPayload(dispute); + } + + public static PaymentAccountPayload getPaymentAccountPayload(Dispute dispute) { + return isBuyer(dispute) ? + dispute.getContract().getBuyerPaymentAccountPayload() : + dispute.getContract().getSellerPaymentAccountPayload(); + } + + public static String getAddress(Dispute dispute) { + return isBuyer(dispute) ? + dispute.getContract().getBuyerNodeAddress().getHostName() : + dispute.getContract().getSellerNodeAddress().getHostName(); + } + + public static String getAckKey(Dispute dispute) { + return ACK_KEY + getSigPubKeyHashAsHex(dispute).substring(0, 4) + "/" + dispute.getShortTradeId(); + } + + private static String getIsBuyerSubString(boolean isBuyer) { + return "'\n Role: " + (isBuyer ? "'Buyer'" : "'Seller'"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final DisputeManager> disputeManager; + + // Key is hex of hash of sig pubKey which we consider a trader identity. We could use onion address as well but + // once we support multiple onion addresses that would not work anymore. + @Getter + private Map> suspiciousDisputesByTraderMap = new HashMap<>(); + private List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public MultipleHolderNameDetection(DisputeManager> disputeManager) { + this.disputeManager = disputeManager; + + disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + detectMultipleHolderNames(); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void detectMultipleHolderNames() { + String previous = suspiciousDisputesByTraderMap.toString(); + getAllDisputesByTraderMap().forEach((key, value) -> { + Set userNames = value.stream() + .map(dispute -> getPayloadWithHolderName(dispute).getHolderName()) + .collect(Collectors.toSet()); + if (userNames.size() > 1) { + // As we compare previous results we need to make sorting deterministic + value.sort(Comparator.comparing(Dispute::getId)); + suspiciousDisputesByTraderMap.put(key, value); + } + }); + String updated = suspiciousDisputesByTraderMap.toString(); + if (!previous.equals(updated)) { + listeners.forEach(Listener::onSuspiciousDisputeDetected); + } + } + + public boolean hasSuspiciousDisputesDetected() { + return !suspiciousDisputesByTraderMap.isEmpty(); + } + + // Returns all disputes of a trader who used multiple names + public List getDisputesForTrader(Dispute dispute) { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + if (suspiciousDisputesByTraderMap.containsKey(traderPubKeyHash)) { + return suspiciousDisputesByTraderMap.get(traderPubKeyHash); + } + return new ArrayList<>(); + } + + // Get a report of traders who used multiple names with all their disputes listed + public String getReportForAllDisputes() { + return getReport(suspiciousDisputesByTraderMap.values()); + } + + // Get a report for a trader who used multiple names with all their disputes listed + public String getReportForDisputeOfTrader(List disputes) { + Collection> values = new ArrayList<>(); + values.add(disputes); + return getReport(values); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map> getAllDisputesByTraderMap() { + Map> allDisputesByTraderMap = new HashMap<>(); + disputeManager.getDisputesAsObservableList() + .forEach(dispute -> { + Contract contract = dispute.getContract(); + PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ? + contract.getBuyerPaymentAccountPayload() : + contract.getSellerPaymentAccountPayload(); + if (paymentAccountPayload instanceof PayloadWithHolderName) { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>()); + List disputes = allDisputesByTraderMap.get(traderPubKeyHash); + disputes.add(dispute); + } + }); + return allDisputesByTraderMap; + } + + // Get a text report for a trader who used multiple names and list all the his disputes + private String getReport(Collection> collectionOfDisputesOfTrader) { + return collectionOfDisputesOfTrader.stream() + .map(disputes -> { + Set addresses = new HashSet<>(); + Set isBuyerHashSet = new HashSet<>(); + Set names = new HashSet<>(); + String disputesReport = disputes.stream() + .map(dispute -> { + addresses.add(getAddress(dispute)); + String ackKey = getAckKey(dispute); + String ackSubString = " "; + if (!DontShowAgainLookup.showAgain(ackKey)) { + ackSubString = "[ACK] "; + } + String holderName = getPayloadWithHolderName(dispute).getHolderName(); + names.add(holderName); + boolean isBuyer = isBuyer(dispute); + isBuyerHashSet.add(isBuyer); + String isBuyerSubString = getIsBuyerSubString(isBuyer); + DisputeResult disputeResult = dispute.disputeResultProperty().get(); + String summaryNotes = disputeResult != null ? disputeResult.getSummaryNotesProperty().get().trim() : "Not closed yet"; + return ackSubString + + "Trade ID: '" + dispute.getShortTradeId() + + "'\n Account holder name: '" + holderName + + "'\n Payment method: '" + Res.get(getPaymentAccountPayload(dispute).getPaymentMethodId()) + + isBuyerSubString + + "'\n Summary: '" + summaryNotes; + }) + .collect(Collectors.joining("\n")); + + String addressSubString = addresses.size() > 1 ? + "used multiple addresses " + addresses + " with" : + "with address " + new ArrayList<>(addresses).get(0) + " used"; + + String roleSubString = "Trader "; + if (isBuyerHashSet.size() == 1) { + boolean isBuyer = new ArrayList<>(isBuyerHashSet).get(0); + String isBuyerSubString = getIsBuyerSubString(isBuyer); + disputesReport = disputesReport.replace(isBuyerSubString, ""); + roleSubString = isBuyer ? "Buyer " : "Seller "; + } + + + String traderReport = roleSubString + addressSubString + " multiple names: " + names.toString() + "\n" + disputesReport; + return new Tuple2<>(roleSubString, traderReport); + }) + .sorted(Comparator.comparing(o -> o.first)) // Buyers first, then seller, then mixed (trader was in seller and buyer role) + .map(e -> e.second) + .collect(Collectors.joining("\n\n")); + } +} diff --git a/core/src/main/java/bisq/core/support/messages/ChatMessage.java b/core/src/main/java/bisq/core/support/messages/ChatMessage.java index 3b38c8a244b..b798a374639 100644 --- a/core/src/main/java/bisq/core/support/messages/ChatMessage.java +++ b/core/src/main/java/bisq/core/support/messages/ChatMessage.java @@ -51,7 +51,7 @@ import javax.annotation.Nullable; /* Message for direct communication between two nodes. Originally built for trader to - * arbitrator communication as no other direct communication was allowed. Aribtrator is + * arbitrator communication as no other direct communication was allowed. Arbitrator is * considered as the server and trader as the client in arbitration chats * * For trader to trader communication the maker is considered to be the server @@ -351,8 +351,7 @@ private void notifyChangeListener() { @Override public String toString() { return "ChatMessage{" + - "\n type='" + supportType + '\'' + - ",\n tradeId='" + tradeId + '\'' + + "\n tradeId='" + tradeId + '\'' + ",\n traderId=" + traderId + ",\n senderIsTrader=" + senderIsTrader + ",\n message='" + message + '\'' + @@ -360,10 +359,9 @@ public String toString() { ",\n senderNodeAddress=" + senderNodeAddress + ",\n date=" + date + ",\n isSystemMessage=" + isSystemMessage + + ",\n wasDisplayed=" + wasDisplayed + ",\n arrivedProperty=" + arrivedProperty + ",\n storedInMailboxProperty=" + storedInMailboxProperty + - ",\n ChatMessage.uid='" + uid + '\'' + - ",\n messageVersion=" + messageVersion + ",\n acknowledgedProperty=" + acknowledgedProperty + ",\n sendMessageErrorProperty=" + sendMessageErrorProperty + ",\n ackErrorProperty=" + ackErrorProperty + diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 8d3c2d738ca..66962f10d68 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -46,8 +46,6 @@ import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.statistics.ReferralIdService; import bisq.core.trade.statistics.TradeStatisticsManager; -import bisq.core.trade.txproof.AssetTxProofResult; -import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.core.user.User; import bisq.core.util.Validator; @@ -59,7 +57,6 @@ import bisq.network.p2p.SendMailboxMessageListener; import bisq.common.ClockWatcher; -import bisq.common.UserThread; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; import bisq.common.handlers.ErrorMessageHandler; @@ -111,8 +108,6 @@ import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkArgument; - public class TradeManager implements PersistedDataHost { private static final Logger log = LoggerFactory.getLogger(TradeManager.class); @@ -132,7 +127,6 @@ public class TradeManager implements PersistedDataHost { private final ReferralIdService referralIdService; private final AccountAgeWitnessService accountAgeWitnessService; @Getter - private final XmrTxProofService xmrTxProofService; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; private final RefundAgentManager refundAgentManager; @@ -174,7 +168,6 @@ public TradeManager(User user, TradeStatisticsManager tradeStatisticsManager, ReferralIdService referralIdService, AccountAgeWitnessService accountAgeWitnessService, - XmrTxProofService xmrTxProofService, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, RefundAgentManager refundAgentManager, @@ -197,7 +190,6 @@ public TradeManager(User user, this.tradeStatisticsManager = tradeStatisticsManager; this.referralIdService = referralIdService; this.accountAgeWitnessService = accountAgeWitnessService; - this.xmrTxProofService = xmrTxProofService; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; this.refundAgentManager = refundAgentManager; @@ -286,7 +278,7 @@ public void onUpdatedDataReceived() { } public void shutDown() { - xmrTxProofService.shutDown(); + // Do nothing here } private void initPendingTrades() { @@ -329,13 +321,6 @@ private void initPendingTrades() { addTradeToFailedTradesList.add(trade); } } - - if (trade.getState() == Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG) { - // This state can be only appear at a SellerTrade - checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade"); - // We delay a bit as at startup lots of stuff is happening - UserThread.runAfter(() -> maybeStartXmrTxProofServices((SellerTrade) trade), 1); - } } ); @@ -360,31 +345,10 @@ private void initPendingTrades() { pendingTradesInitialized.set(true); } - public void maybeStartXmrTxProofServices(SellerTrade sellerTrade) { - xmrTxProofService.maybeStartRequests(sellerTrade, tradableList.getList(), - assetTxProofResult -> { - if (assetTxProofResult == AssetTxProofResult.COMPLETED) { - log.info("###########################################################################################"); - log.info("We auto-confirm trade {} as our all our services for the tx proof completed successfully", sellerTrade.getShortId()); - log.info("###########################################################################################"); - autoConfirmFiatPaymentReceived(sellerTrade); - } - }, - (errorMessage, throwable) -> { - log.error(errorMessage); - }); - } - - private void autoConfirmFiatPaymentReceived(SellerTrade sellerTrade) { - onFiatPaymentReceived(sellerTrade, - () -> { - }, errorMessage -> { - }); - } - public void onFiatPaymentReceived(SellerTrade sellerTrade, - ResultHandler resultHandler, - ErrorMessageHandler errorMessageHandler) { + public void onUserConfirmedFiatPaymentReceived(SellerTrade sellerTrade, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { sellerTrade.onFiatPaymentReceived(resultHandler, errorMessageHandler); //TODO move to trade protocol task diff --git a/core/src/main/java/bisq/core/trade/TradeModule.java b/core/src/main/java/bisq/core/trade/TradeModule.java index 422ec13a83c..779cf6e7600 100644 --- a/core/src/main/java/bisq/core/trade/TradeModule.java +++ b/core/src/main/java/bisq/core/trade/TradeModule.java @@ -26,8 +26,6 @@ import bisq.core.trade.statistics.ReferralIdService; import bisq.core.trade.statistics.TradeStatistics2StorageService; import bisq.core.trade.statistics.TradeStatisticsManager; -import bisq.core.trade.txproof.AssetTxProofHttpClient; -import bisq.core.trade.txproof.xmr.XmrTxProofHttpClient; import bisq.common.app.AppModule; import bisq.common.config.Config; @@ -58,8 +56,6 @@ protected void configure() { bind(SignedWitnessStorageService.class).in(Singleton.class); bind(ReferralIdService.class).in(Singleton.class); - bind(AssetTxProofHttpClient.class).to(XmrTxProofHttpClient.class); - bindConstant().annotatedWith(named(DUMP_STATISTICS)).to(config.dumpStatistics); bindConstant().annotatedWith(named(DUMP_DELAYED_PAYOUT_TXS)).to(config.dumpDelayedPayoutTxs); bindConstant().annotatedWith(named(ALLOW_FAULTY_DELAYED_TXS)).to(config.allowFaultyDelayedTxs); diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index ec5d5547937..6f475f6734e 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -55,6 +55,8 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; + @Slf4j public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol, MakerProtocol { private final BuyerAsMakerTrade buyerAsMakerTrade; @@ -212,6 +214,9 @@ private void handle() { // User clicked the "bank transfer started" button @Override public void onFiatPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + checkArgument(!wasDisputed(), "A call to onFiatPaymentStarted is not permitted once a " + + "dispute has been opened."); + if (trade.isDepositConfirmed() && !trade.isFiatSent()) { buyerAsMakerTrade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED); TradeTaskRunner taskRunner = new TradeTaskRunner(buyerAsMakerTrade, diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index 6665c001ec6..4d17d4c47fa 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -59,6 +59,7 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -237,6 +238,9 @@ private void handle() { // User clicked the "bank transfer started" button @Override public void onFiatPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + checkArgument(!wasDisputed(), "A call to onFiatPaymentStarted is not permitted once a " + + "dispute has been opened."); + if (!trade.isFiatSent()) { buyerAsTakerTrade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED); diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index 3a5ae4df9b8..29ecde1c005 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -57,6 +57,8 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; + @Slf4j public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtocol, MakerProtocol { private final SellerAsMakerTrade sellerAsMakerTrade; @@ -204,6 +206,9 @@ private void handle(CounterCurrencyTransferStartedMessage tradeMessage, NodeAddr // User clicked the "bank transfer received" button, so we release the funds for payout @Override public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + checkArgument(!wasDisputed(), "A call to onFiatPaymentReceived is not permitted once a " + + "dispute has been opened."); + if (trade.getPayoutTx() == null) { sellerAsMakerTrade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT); TradeTaskRunner taskRunner = new TradeTaskRunner(sellerAsMakerTrade, diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index fd42d66b7c0..8979a61b28c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -56,6 +56,7 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -196,6 +197,9 @@ private void handle(CounterCurrencyTransferStartedMessage tradeMessage, NodeAddr // User clicked the "bank transfer received" button, so we release the funds for payout @Override public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + checkArgument(!wasDisputed(), "A call to onFiatPaymentReceived is not permitted once a " + + "dispute has been opened."); + if (trade.getPayoutTx() == null) { sellerAsTakerTrade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT); TradeTaskRunner taskRunner = new TradeTaskRunner(sellerAsTakerTrade, diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index 4cf78045db2..c561b242741 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -345,6 +345,10 @@ protected void handleTaskRunnerFault(@Nullable TradeMessage tradeMessage, String cleanup(); } + protected boolean wasDisputed() { + return trade.getDisputeState() != Trade.DisputeState.NO_DISPUTE; + } + private void sendAckMessage(@Nullable TradeMessage tradeMessage, boolean result, @Nullable String errorMessage) { // We complete at initial protocol setup with the setup listener tasks. // Other cases are if we start from an UI event the task runner (payment started, confirmed). diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java index 29ca5bc63ea..ee996ffd9c1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java @@ -17,7 +17,6 @@ package bisq.core.trade.protocol.tasks.seller; -import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; import bisq.core.trade.protocol.tasks.TradeTask; @@ -27,7 +26,6 @@ import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -62,10 +60,6 @@ protected void run() { trade.setCounterCurrencyExtraData(counterCurrencyExtraData); } - checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade"); - // We return early in the service if its not XMR. We prefer to not have additional checks outside... - processModel.getTradeManager().maybeStartXmrTxProofServices((SellerTrade) trade); - processModel.removeMailboxMessageAfterProcessing(trade); trade.setState(Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG); diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java index c732a077f2c..e2768a1be9a 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2StorageService.java @@ -20,6 +20,7 @@ import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.payload.PersistableNetworkPayload; import bisq.network.p2p.storage.persistence.MapStoreService; +import bisq.network.p2p.storage.persistence.SplitStoreService; import bisq.common.config.Config; import bisq.common.storage.Storage; @@ -35,7 +36,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class TradeStatistics2StorageService extends MapStoreService { +public class TradeStatistics2StorageService extends SplitStoreService { private static final String FILE_NAME = "TradeStatistics2Store"; @@ -59,11 +60,6 @@ public String getFileName() { return FILE_NAME; } - @Override - public Map getMap() { - return store.getMap(); - } - @Override public boolean canHandle(PersistableNetworkPayload payload) { return payload instanceof TradeStatistics2; @@ -78,9 +74,4 @@ public boolean canHandle(PersistableNetworkPayload payload) { protected TradeStatistics2Store createStore() { return new TradeStatistics2Store(); } - - @Override - protected void readStore() { - super.readStore(); - } } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java index d3ea468907c..bb2354a8fce 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2Store.java @@ -18,18 +18,13 @@ package bisq.core.trade.statistics; import bisq.network.p2p.storage.P2PDataStorage; -import bisq.network.p2p.storage.payload.PersistableNetworkPayload; - -import bisq.common.proto.persistable.ThreadedPersistableEnvelope; +import bisq.network.p2p.storage.persistence.SplitStore; import com.google.protobuf.Message; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** @@ -38,10 +33,7 @@ * definition and provide a hashMap for the domain access. */ @Slf4j -public class TradeStatistics2Store implements ThreadedPersistableEnvelope { - @Getter - private Map map = new ConcurrentHashMap<>(); - +public class TradeStatistics2Store extends SplitStore { TradeStatistics2Store() { } diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java index 0fc61d7cfd0..b49d81b6609 100644 --- a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofResult.java @@ -26,6 +26,7 @@ public enum AssetTxProofResult { TRADE_LIMIT_EXCEEDED, INVALID_DATA, // Peer provided invalid data. Might be a scam attempt (e.g. txKey reused) PAYOUT_TX_ALREADY_PUBLISHED, + DISPUTE_OPENED, REQUESTS_STARTED(false), PENDING(false), @@ -39,15 +40,20 @@ public enum AssetTxProofResult { // Any service failed. Might be that the tx is invalid. FAILED; + // If isTerminal is set it means that we stop the service + @Getter + private final boolean isTerminal; + @Getter + private String details = ""; @Getter private int numSuccessResults; @Getter private int numRequiredSuccessResults; @Getter - private String details = ""; - // If isTerminal is set it means that we stop the service + private int numConfirmations; @Getter - private final boolean isTerminal; + private int numRequiredConfirmations; + AssetTxProofResult() { this(true); @@ -68,6 +74,16 @@ public AssetTxProofResult numRequiredSuccessResults(int numRequiredSuccessResult return this; } + public AssetTxProofResult numConfirmations(int numConfirmations) { + this.numConfirmations = numConfirmations; + return this; + } + + public AssetTxProofResult numRequiredConfirmations(int numRequiredConfirmations) { + this.numRequiredConfirmations = numRequiredConfirmations; + return this; + } + public AssetTxProofResult details(String details) { this.details = details; return this; @@ -76,9 +92,12 @@ public AssetTxProofResult details(String details) { @Override public String toString() { return "AssetTxProofResult{" + - "\n numSuccessResults=" + numSuccessResults + - ",\n requiredSuccessResults=" + numRequiredSuccessResults + - ",\n details='" + details + '\'' + + "\n details='" + details + '\'' + + ",\n isTerminal=" + isTerminal + + ",\n numSuccessResults=" + numSuccessResults + + ",\n numRequiredSuccessResults=" + numRequiredSuccessResults + + ",\n numConfirmations=" + numConfirmations + + ",\n numRequiredConfirmations=" + numRequiredConfirmations + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java index 56e2a7462e8..c5c8476a41b 100644 --- a/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/AssetTxProofService.java @@ -17,18 +17,8 @@ package bisq.core.trade.txproof; -import bisq.core.trade.Trade; - -import bisq.common.handlers.FaultHandler; - -import java.util.List; -import java.util.function.Consumer; - public interface AssetTxProofService { - void maybeStartRequests(Trade trade, - List activeTrades, - Consumer resultHandler, - FaultHandler faultHandler); + void onAllServicesInitialized(); void shutDown(); } diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/DevTestXmrTxProofHttpClient.java b/core/src/main/java/bisq/core/trade/txproof/xmr/DevTestXmrTxProofHttpClient.java deleted file mode 100644 index b415d354c44..00000000000 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/DevTestXmrTxProofHttpClient.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.core.trade.txproof.xmr; - -import bisq.core.trade.txproof.AssetTxProofHttpClient; - -import bisq.network.Socks5ProxyProvider; -import bisq.network.http.HttpClientImpl; - -import bisq.common.app.DevEnv; - -import javax.inject.Inject; - -import java.util.Date; - -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nullable; - -import static bisq.core.trade.txproof.xmr.XmrTxProofParser.MAX_DATE_TOLERANCE; - -/** - * This should help to test error scenarios in dev testing the app. This is additional to unit test which test the - * correct data but do not test the context of the results and how it behaves in the UI. - * - * You have to change the binding in TradeModule to - * bind(AssetTxProofHttpClient.class).to(DevTestXmrTxProofHttpClient.class); to use that class. - * - * This class can be removed once done testing, but as multiple devs are testing its useful to share it for now. - */ -@Slf4j -public class DevTestXmrTxProofHttpClient extends HttpClientImpl implements AssetTxProofHttpClient { - enum ApiInvalidDetails { - EMPTY_JSON, - MISSING_DATA, - MISSING_STATUS, - UNHANDLED_STATUS, - MISSING_ADDRESS, - MISSING_TX_ID, - MISSING_VIEW_KEY, - MISSING_TS, - MISSING_CONF, - EXCEPTION - } - - @Inject - public DevTestXmrTxProofHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) { - super(socks5ProxyProvider); - } - - private static int counter; - - @Override - public String requestWithGET(String param, - @Nullable String headerKey, - @Nullable String headerValue) { - - XmrTxProofRequest.Result result = XmrTxProofRequest.Result.PENDING; - XmrTxProofRequest.Detail detail = XmrTxProofRequest.Detail.TX_NOT_FOUND; - ApiInvalidDetails apiInvalidDetails = ApiInvalidDetails.EXCEPTION; - - int delay = counter == 0 ? 2000 : 100; - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - if (counter >= 2) { - detail = XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS.numConfirmations(counter - 2); - } - counter++; - switch (result) { - case PENDING: - switch (detail) { - case TX_NOT_FOUND: - return validJson().replace("success", - "fail"); - case PENDING_CONFIRMATIONS: - return validJson().replace("201287", - String.valueOf(detail.getNumConfirmations())); - default: - return null; - } - case SUCCESS: - return validJson(); - case FAILED: - switch (detail) { - case TX_HASH_INVALID: - return validJson().replace("5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802", - "-"); - case TX_KEY_INVALID: - return validJson().replace("f3ce66c9d395e5e460c8802b2c3c1fff04e508434f9738ee35558aac4678c906", - "-"); - case ADDRESS_INVALID: - return validJson().replace("590f7263428051068bb45cdfcf93407c15b6e291d20c92d0251fcfbf53cc745cdf53319f7d6d7a8e21ea39041aabf31d220a32a875e3ca2087a777f1201c0571", - "-"); - case NO_MATCH_FOUND: - return validJson().replace("match\": true", - "match\": false"); - case AMOUNT_NOT_MATCHING: - return validJson().replace("8902597360000", - "18902597360000"); - case TRADE_DATE_NOT_MATCHING: - DevEnv.setDevMode(false); - long date = (new Date(1574922644 * 1000L).getTime() - (MAX_DATE_TOLERANCE * 1000L + 1)) / 1000; - return validJson().replace("1574922644", - String.valueOf(date)); - default: - return null; - } - case ERROR: - switch (detail) { - case CONNECTION_FAILURE: - // Not part of parser level testing - return null; - case API_INVALID: - switch (apiInvalidDetails) { - case EMPTY_JSON: - return null; - case MISSING_DATA: - return validJson().replace("data", - "missing"); - case MISSING_STATUS: - return validJson().replace("status", - "missing"); - case UNHANDLED_STATUS: - return validJson().replace("success", - "missing"); - case MISSING_ADDRESS: - return validJson().replace("address", - "missing"); - case MISSING_TX_ID: - return validJson().replace("tx_hash", - "missing"); - case MISSING_VIEW_KEY: - return validJson().replace("viewkey", - "missing"); - case MISSING_TS: - return validJson().replace("tx_timestamp", - "missing"); - case MISSING_CONF: - return validJson().replace("tx_confirmations", - "missing"); - case EXCEPTION: - return validJson().replace("} ", - ""); - default: - return null; - } - - case NO_RESULTS_TIMEOUT: - // Not part of parser level testing - return null; - default: - return null; - } - default: - return null; - } - } - - private String validJson() { - return "{\n" + - " \"data\": {\n" + - " \"address\": \"590f7263428051068bb45cdfcf93407c15b6e291d20c92d0251fcfbf53cc745cdf53319f7d6d7a8e21ea39041aabf31d220a32a875e3ca2087a777f1201c0571\",\n" + - " \"outputs\": [\n" + - " {\n" + - " \"amount\": 8902597360000,\n" + - " \"match\": true,\n" + - " \"output_idx\": 0,\n" + - " \"output_pubkey\": \"2b6d2296f2591c198cd1aa47de9a5d74270963412ed30bbcc63b8eff29f0d43e\"\n" + - " },\n" + - " {\n" + - " \"amount\": 0,\n" + - " \"match\": false,\n" + - " \"output_idx\": 1,\n" + - " \"output_pubkey\": \"f53271624847507d80b746e91e689e88bc41678d55246275f5ad3c0f7e8a9ced\"\n" + - " }\n" + - " ],\n" + - " \"tx_confirmations\": 201287,\n" + - " \"tx_hash\": \"5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802\",\n" + - " \"tx_prove\": true,\n" + - " \"tx_timestamp\": 1574922644,\n" + - " \"viewkey\": \"f3ce66c9d395e5e460c8802b2c3c1fff04e508434f9738ee35558aac4678c906\"\n" + - " },\n" + - " \"status\": \"success\"\n" + - "} "; - } - -} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java index dce87473837..ac0a4148de0 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofHttpClient.java @@ -22,14 +22,11 @@ import bisq.network.Socks5ProxyProvider; import bisq.network.http.HttpClientImpl; -import javax.inject.Inject; - import lombok.extern.slf4j.Slf4j; @Slf4j -public class XmrTxProofHttpClient extends HttpClientImpl implements AssetTxProofHttpClient { - @Inject - public XmrTxProofHttpClient(Socks5ProxyProvider socks5ProxyProvider) { +class XmrTxProofHttpClient extends HttpClientImpl implements AssetTxProofHttpClient { + XmrTxProofHttpClient(Socks5ProxyProvider socks5ProxyProvider) { super(socks5ProxyProvider); } } diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java index cf7b7835cfc..5a11cbc5c6b 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofParser.java @@ -162,7 +162,7 @@ public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) { if (confirmations < confirmsRequired) { return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS.numConfirmations(confirmations)); } else { - return XmrTxProofRequest.Result.SUCCESS; + return XmrTxProofRequest.Result.SUCCESS.with(XmrTxProofRequest.Detail.SUCCESS.numConfirmations(confirmations)); } } catch (JsonParseException | NullPointerException e) { diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java index c853af2410c..eb62b09468f 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequest.java @@ -21,6 +21,8 @@ import bisq.core.trade.txproof.AssetTxProofParser; import bisq.core.trade.txproof.AssetTxProofRequest; +import bisq.network.Socks5ProxyProvider; + import bisq.common.UserThread; import bisq.common.app.Version; import bisq.common.handlers.FaultHandler; @@ -85,6 +87,8 @@ enum Detail { TX_NOT_FOUND, // Tx not visible in network yet. Could be also other error PENDING_CONFIRMATIONS, + SUCCESS, + // Error states CONNECTION_FAILURE, API_INVALID, @@ -138,9 +142,9 @@ public String toString() { private final ListeningExecutorService executorService = Utilities.getListeningExecutorService( "XmrTransferProofRequester", 3, 5, 10 * 60); - private final AssetTxProofHttpClient httpClient; private final AssetTxProofParser parser; private final XmrTxProofModel model; + private final AssetTxProofHttpClient httpClient; private final long firstRequest; private boolean terminated; @@ -153,16 +157,25 @@ public String toString() { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - XmrTxProofRequest(AssetTxProofHttpClient httpClient, + XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider, XmrTxProofModel model) { - this.httpClient = httpClient; this.parser = new XmrTxProofParser(); this.model = model; - httpClient.setBaseUrl("http://" + model.getServiceAddress()); - if (model.getServiceAddress().matches("^192.*|^localhost.*")) { - log.info("Ignoring Socks5 proxy for local net address: {}", model.getServiceAddress()); + httpClient = new XmrTxProofHttpClient(socks5ProxyProvider); + + // localhost, LAN address, or *.local FQDN starts with http://, don't use Tor + if (model.getServiceAddress().regionMatches(0, "http:", 0, 5)) { + httpClient.setBaseUrl(model.getServiceAddress()); httpClient.setIgnoreSocks5Proxy(true); + // any non-onion FQDN starts with https://, use Tor + } else if (model.getServiceAddress().regionMatches(0, "https:", 0, 6)) { + httpClient.setBaseUrl(model.getServiceAddress()); + httpClient.setIgnoreSocks5Proxy(false); + // it's a raw onion so add http:// and use Tor proxy + } else { + httpClient.setBaseUrl("http://" + model.getServiceAddress()); + httpClient.setIgnoreSocks5Proxy(false); } terminated = false; @@ -197,7 +210,7 @@ public void requestFromService(Consumer resultHandler, FaultHandler faul String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json)); log.info("Response json from {}\n{}", this, prettyJson); } catch (Throwable error) { - log.error("Pretty rint caused a {}}: raw josn={}", error, json); + log.error("Pretty print caused a {}: raw json={}", error, json); } Result result = parser.parse(model, json); @@ -268,12 +281,10 @@ public String toString() { /////////////////////////////////////////////////////////////////////////////////////////// private String getShortId() { - return Utilities.getShortId(model.getTradeId()) + " @ " + - model.getServiceAddress().substring(0, 6); + return Utilities.getShortId(model.getTradeId()) + " @ " + model.getServiceAddress().substring(0, 6); } private boolean isTimeOutReached() { return System.currentTimeMillis() - firstRequest > MAX_REQUEST_PERIOD; } - } diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java index f49919232b5..d7a1e84bc1f 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofRequestsPerTrade.java @@ -18,18 +18,25 @@ package bisq.core.trade.txproof.xmr; import bisq.core.locale.Res; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Trade; -import bisq.core.trade.txproof.AssetTxProofHttpClient; import bisq.core.trade.txproof.AssetTxProofRequestsPerTrade; import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.user.AutoConfirmSettings; +import bisq.network.Socks5ProxyProvider; + import bisq.common.handlers.FaultHandler; import org.bitcoinj.core.Coin; import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -46,7 +53,9 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { @Getter private final Trade trade; private final AutoConfirmSettings autoConfirmSettings; - private final AssetTxProofHttpClient httpClient; + private final MediationManager mediationManager; + private final RefundManager refundManager; + private final Socks5ProxyProvider socks5ProxyProvider; private int numRequiredSuccessResults; private final Set requests = new HashSet<>(); @@ -54,18 +63,23 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { private int numSuccessResults; private ChangeListener tradeStateListener; private AutoConfirmSettings.Listener autoConfirmSettingsListener; + private ListChangeListener mediationListener, refundListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - XmrTxProofRequestsPerTrade(AssetTxProofHttpClient httpClient, + XmrTxProofRequestsPerTrade(Socks5ProxyProvider socks5ProxyProvider, Trade trade, - AutoConfirmSettings autoConfirmSettings) { - this.httpClient = httpClient; + AutoConfirmSettings autoConfirmSettings, + MediationManager mediationManager, + RefundManager refundManager) { + this.socks5ProxyProvider = socks5ProxyProvider; this.trade = trade; this.autoConfirmSettings = autoConfirmSettings; + this.mediationManager = mediationManager; + this.refundManager = refundManager; } @@ -75,52 +89,70 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade { @Override public void requestFromAllServices(Consumer resultHandler, FaultHandler faultHandler) { - // We set serviceAddresses at request time. If user changes AutoConfirmSettings after request has started - // it will have no impact on serviceAddresses and numRequiredSuccessResults. - // Thought numRequiredConfirmations can be changed during request process and will be read from - // autoConfirmSettings at result parsing. - List serviceAddresses = autoConfirmSettings.getServiceAddresses(); - numRequiredSuccessResults = serviceAddresses.size(); - + // isTradeAmountAboveLimit if (isTradeAmountAboveLimit(trade)) { callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.TRADE_LIMIT_EXCEEDED); return; } + // isPayoutPublished if (trade.isPayoutPublished()) { callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED); return; } + // IsEnabled() // We will stop all our services if the user changes the enable state in the AutoConfirmSettings - autoConfirmSettingsListener = () -> { - if (!autoConfirmSettings.isEnabled()) { - callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED); - } - }; - autoConfirmSettings.addListener(autoConfirmSettingsListener); if (!autoConfirmSettings.isEnabled()) { callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED); return; } + addSettingsListener(resultHandler); - tradeStateListener = (observable, oldValue, newValue) -> { - if (trade.isPayoutPublished()) { - callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED); - } - }; - trade.stateProperty().addListener(tradeStateListener); + // TradeState + setupTradeStateListener(resultHandler); + // We checked initially for current trade state so no need to check again here + + // Check if mediation dispute and add listener + ObservableList mediationDisputes = mediationManager.getDisputesAsObservableList(); + if (isDisputed(mediationDisputes)) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + return; + } + setupMediationListener(resultHandler, mediationDisputes); + + // Check if arbitration dispute and add listener + ObservableList refundDisputes = refundManager.getDisputesAsObservableList(); + if (isDisputed(refundDisputes)) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + return; + } + setupArbitrationListener(resultHandler, refundDisputes); + // All good so we start callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.REQUESTS_STARTED); + // We set serviceAddresses at request time. If user changes AutoConfirmSettings after request has started + // it will have no impact on serviceAddresses and numRequiredSuccessResults. + // Thought numRequiredConfirmations can be changed during request process and will be read from + // autoConfirmSettings at result parsing. + List serviceAddresses = autoConfirmSettings.getServiceAddresses(); + numRequiredSuccessResults = serviceAddresses.size(); + for (String serviceAddress : serviceAddresses) { XmrTxProofModel model = new XmrTxProofModel(trade, serviceAddress, autoConfirmSettings); - XmrTxProofRequest request = new XmrTxProofRequest(httpClient, model); + XmrTxProofRequest request = new XmrTxProofRequest(socks5ProxyProvider, model); log.info("{} created", request); requests.add(request); request.requestFromService(result -> { + // If we ever received an error or failed result we terminate and do not process any + // future result anymore to avoid that we overwrite out state with success. + if (wasTerminated()) { + return; + } + AssetTxProofResult assetTxProofResult; if (trade.isPayoutPublished()) { assetTxProofResult = AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED; @@ -146,7 +178,12 @@ public void requestFromAllServices(Consumer resultHandler, F // have completed on the service level. log.info("All {} tx proof requests for trade {} have been successful.", numRequiredSuccessResults, trade.getShortId()); - assetTxProofResult = AssetTxProofResult.COMPLETED; + XmrTxProofRequest.Detail detail = result.getDetail(); + assetTxProofResult = AssetTxProofResult.COMPLETED + .numSuccessResults(numSuccessResults) + .numRequiredSuccessResults(numRequiredSuccessResults) + .numConfirmations(detail != null ? detail.getNumConfirmations() : 0) + .numRequiredConfirmations(autoConfirmSettings.getRequiredConfirmations()); } break; case FAILED: @@ -174,16 +211,70 @@ public void requestFromAllServices(Consumer resultHandler, F } } + private boolean wasTerminated() { + return requests.isEmpty(); + } + + private void addSettingsListener(Consumer resultHandler) { + autoConfirmSettingsListener = () -> { + if (!autoConfirmSettings.isEnabled()) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED); + } + }; + autoConfirmSettings.addListener(autoConfirmSettingsListener); + } + + private void setupTradeStateListener(Consumer resultHandler) { + tradeStateListener = (observable, oldValue, newValue) -> { + if (trade.isPayoutPublished()) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED); + } + }; + trade.stateProperty().addListener(tradeStateListener); + } + + private void setupArbitrationListener(Consumer resultHandler, + ObservableList refundDisputes) { + refundListener = c -> { + c.next(); + if (c.wasAdded() && isDisputed(c.getAddedSubList())) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + } + }; + refundDisputes.addListener(refundListener); + } + + private void setupMediationListener(Consumer resultHandler, + ObservableList mediationDisputes) { + mediationListener = c -> { + c.next(); + if (c.wasAdded() && isDisputed(c.getAddedSubList())) { + callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED); + } + }; + mediationDisputes.addListener(mediationListener); + } + @Override public void terminate() { requests.forEach(XmrTxProofRequest::terminate); requests.clear(); + if (tradeStateListener != null) { trade.stateProperty().removeListener(tradeStateListener); } + if (autoConfirmSettingsListener != null) { autoConfirmSettings.removeListener(autoConfirmSettingsListener); } + + if (mediationListener != null) { + mediationManager.getDisputesAsObservableList().removeListener(mediationListener); + } + + if (refundListener != null) { + refundManager.getDisputesAsObservableList().removeListener(refundListener); + } } @@ -217,6 +308,8 @@ private AssetTxProofResult getAssetTxProofResultForPending(XmrTxProofRequest.Res return AssetTxProofResult.PENDING .numSuccessResults(numSuccessResults) .numRequiredSuccessResults(numRequiredSuccessResults) + .numConfirmations(detail != null ? detail.getNumConfirmations() : 0) + .numRequiredConfirmations(autoConfirmSettings.getRequiredConfirmations()) .details(detailString); } @@ -235,4 +328,8 @@ private boolean isTradeAmountAboveLimit(Trade trade) { } return false; } + + private boolean isDisputed(List disputes) { + return disputes.stream().anyMatch(e -> e.getTradeId().equals(trade.getId())); + } } diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index bd63e1df114..c3ec7de2843 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -20,30 +20,40 @@ import bisq.core.btc.setup.WalletsSetup; import bisq.core.filter.FilterManager; import bisq.core.locale.Res; -import bisq.core.payment.payload.AssetsAccountPayload; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; -import bisq.core.trade.txproof.AssetTxProofHttpClient; import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.trade.txproof.AssetTxProofService; import bisq.core.user.AutoConfirmSettings; import bisq.core.user.Preferences; +import bisq.network.Socks5ProxyProvider; +import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.P2PService; import bisq.common.app.DevEnv; -import bisq.common.handlers.FaultHandler; import javax.inject.Inject; import javax.inject.Singleton; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -59,12 +69,21 @@ public class XmrTxProofService implements AssetTxProofService { private final FilterManager filterManager; private final Preferences preferences; + private final TradeManager tradeManager; private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; + private final MediationManager mediationManager; + private final RefundManager refundManager; private final P2PService p2PService; private final WalletsSetup walletsSetup; - private final AssetTxProofHttpClient httpClient; + private final Socks5ProxyProvider socks5ProxyProvider; private final Map servicesByTradeId = new HashMap<>(); + private AutoConfirmSettings autoConfirmSettings; + private Map> tradeStateListenerMap = new HashMap<>(); + private ChangeListener btcPeersListener, btcBlockListener; + private BootstrapListener bootstrapListener; + private MonadicBinding p2pNetworkAndWalletReady; + private ChangeListener p2pNetworkAndWalletReadyListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -75,19 +94,82 @@ public class XmrTxProofService implements AssetTxProofService { @Inject public XmrTxProofService(FilterManager filterManager, Preferences preferences, + TradeManager tradeManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, + MediationManager mediationManager, + RefundManager refundManager, P2PService p2PService, WalletsSetup walletsSetup, - AssetTxProofHttpClient httpClient) { + Socks5ProxyProvider socks5ProxyProvider) { this.filterManager = filterManager; this.preferences = preferences; + this.tradeManager = tradeManager; this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; + this.mediationManager = mediationManager; + this.refundManager = refundManager; this.p2PService = p2PService; this.walletsSetup = walletsSetup; - this.httpClient = httpClient; + this.socks5ProxyProvider = socks5ProxyProvider; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onAllServicesInitialized() { + // As we might trigger the payout tx we want to be sure that we are well connected to the Bitcoin network. + // onAllServicesInitialized is called once we have received the initial data but we want to have our + // hidden service published and upDatedDataResponse received before we start. + BooleanProperty isP2pBootstrapped = isP2pBootstrapped(); + BooleanProperty hasSufficientBtcPeers = hasSufficientBtcPeers(); + BooleanProperty isBtcBlockDownloadComplete = isBtcBlockDownloadComplete(); + if (isP2pBootstrapped.get() && hasSufficientBtcPeers.get() && isBtcBlockDownloadComplete.get()) { + onP2pNetworkAndWalletReady(); + } else { + p2pNetworkAndWalletReady = EasyBind.combine(isP2pBootstrapped, hasSufficientBtcPeers, isBtcBlockDownloadComplete, + (bootstrapped, sufficientPeers, downloadComplete) -> { + log.info("isP2pBootstrapped={}, hasSufficientBtcPeers={} isBtcBlockDownloadComplete={}", + bootstrapped, sufficientPeers, downloadComplete); + return bootstrapped && sufficientPeers && downloadComplete; + }); + + p2pNetworkAndWalletReadyListener = (observable, oldValue, newValue) -> { + if (newValue) { + onP2pNetworkAndWalletReady(); + } + }; + p2pNetworkAndWalletReady.subscribe(p2pNetworkAndWalletReadyListener); + } + } + + @Override + public void shutDown() { + servicesByTradeId.values().forEach(XmrTxProofRequestsPerTrade::terminate); + servicesByTradeId.clear(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onP2pNetworkAndWalletReady() { + if (p2pNetworkAndWalletReady != null) { + p2pNetworkAndWalletReady.removeListener(p2pNetworkAndWalletReadyListener); + p2pNetworkAndWalletReady = null; + p2pNetworkAndWalletReadyListener = null; + } + + if (!preferences.findAutoConfirmSettings("XMR").isPresent()) { + log.error("AutoConfirmSettings is not present"); + } + autoConfirmSettings = preferences.findAutoConfirmSettings("XMR").get(); + + // We register a listener to stop running services. For new trades we check anyway in the trade validation filterManager.filterProperty().addListener((observable, oldValue, newValue) -> { if (isAutoConfDisabledByFilter()) { servicesByTradeId.values().stream().map(XmrTxProofRequestsPerTrade::getTrade).forEach(trade -> @@ -96,22 +178,52 @@ public XmrTxProofService(FilterManager filterManager, shutDown(); } }); + + // We listen on new trades + ObservableList tradableList = tradeManager.getTradableList(); + tradableList.addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + processTrades(c.getAddedSubList()); + } + }); + + // Process existing trades + processTrades(tradableList); } + private void processTrades(List trades) { + trades.stream() + .filter(trade -> trade instanceof SellerTrade) + .map(trade -> (SellerTrade) trade) + .filter(this::isXmrTrade) + .filter(trade -> !trade.isFiatReceived()) // Phase name is from the time when it was fiat only. Means counter currency (XMR) received. + .forEach(this::processTradeOrAddListener); + } - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// + // Basic requirements are fulfilled. + // We process further if we are in the expected state or register a listener + private void processTradeOrAddListener(SellerTrade trade) { + if (isExpectedTradeState(trade.getState())) { + startRequestsIfValid(trade); + } else { + // We are expecting SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG in the future, so listen on changes + ChangeListener tradeStateListener = (observable, oldValue, newValue) -> { + if (isExpectedTradeState(newValue)) { + ChangeListener listener = tradeStateListenerMap.remove(trade.getId()); + if (listener != null) { + trade.stateProperty().removeListener(listener); + } - @Override - public void maybeStartRequests(Trade trade, - List activeTrades, - Consumer resultHandler, - FaultHandler faultHandler) { - if (!isXmrBuyer(trade)) { - return; + startRequestsIfValid(trade); + } + }; + tradeStateListenerMap.put(trade.getId(), tradeStateListener); + trade.stateProperty().addListener(tradeStateListener); } + } + private void startRequestsIfValid(SellerTrade trade) { String txId = trade.getCounterCurrencyTxId(); String txHash = trade.getCounterCurrencyExtraData(); if (is32BitHexStringInValid(txId) || is32BitHexStringInValid(txHash)) { @@ -119,68 +231,116 @@ public void maybeStartRequests(Trade trade, return; } - if (!networkAndWalletReady()) { - return; - } - - Optional optionalAutoConfirmSettings = preferences.findAutoConfirmSettings("XMR"); - if (!optionalAutoConfirmSettings.isPresent()) { - // Not expected - log.error("autoConfirmSettings is not present"); - return; - } - AutoConfirmSettings autoConfirmSettings = optionalAutoConfirmSettings.get(); - if (isAutoConfDisabledByFilter()) { trade.setAssetTxProofResult(AssetTxProofResult.FEATURE_DISABLED .details(Res.get("portfolio.pending.autoConf.state.filterDisabledFeature"))); return; } - if (wasTxKeyReUsed(trade, activeTrades)) { + if (wasTxKeyReUsed(trade, tradeManager.getTradableList())) { trade.setAssetTxProofResult(AssetTxProofResult.INVALID_DATA .details(Res.get("portfolio.pending.autoConf.state.xmr.txKeyReused"))); return; } - XmrTxProofRequestsPerTrade service = new XmrTxProofRequestsPerTrade(httpClient, + startRequests(trade); + } + + private void startRequests(SellerTrade trade) { + XmrTxProofRequestsPerTrade service = new XmrTxProofRequestsPerTrade(socks5ProxyProvider, trade, - autoConfirmSettings); + autoConfirmSettings, + mediationManager, + refundManager); servicesByTradeId.put(trade.getId(), service); service.requestFromAllServices( assetTxProofResult -> { trade.setAssetTxProofResult(assetTxProofResult); + if (assetTxProofResult == AssetTxProofResult.COMPLETED) { + log.info("###########################################################################################"); + log.info("We auto-confirm trade {} as our all our services for the tx proof completed successfully", trade.getShortId()); + log.info("###########################################################################################"); + + trade.onFiatPaymentReceived(() -> { + }, errorMessage -> { + }); + } + if (assetTxProofResult.isTerminal()) { servicesByTradeId.remove(trade.getId()); } - - resultHandler.accept(assetTxProofResult); }, - faultHandler); - } - - @Override - public void shutDown() { - servicesByTradeId.values().forEach(XmrTxProofRequestsPerTrade::terminate); - servicesByTradeId.clear(); + (errorMessage, throwable) -> { + log.error(errorMessage); + }); } /////////////////////////////////////////////////////////////////////////////////////////// - // Validation + // Startup checks /////////////////////////////////////////////////////////////////////////////////////////// - private boolean isXmrBuyer(Trade trade) { - if (!checkNotNull(trade.getOffer()).getCurrencyCode().equals("XMR")) { - return false; + private BooleanProperty isBtcBlockDownloadComplete() { + BooleanProperty result = new SimpleBooleanProperty(); + if (walletsSetup.isDownloadComplete()) { + result.set(true); + } else { + btcBlockListener = (observable, oldValue, newValue) -> { + if (walletsSetup.isDownloadComplete()) { + walletsSetup.downloadPercentageProperty().removeListener(btcBlockListener); + result.set(true); + } + }; + walletsSetup.downloadPercentageProperty().addListener(btcBlockListener); } + return result; + } - if (!(trade instanceof SellerTrade)) { - return false; + private BooleanProperty hasSufficientBtcPeers() { + BooleanProperty result = new SimpleBooleanProperty(); + if (walletsSetup.hasSufficientPeersForBroadcast()) { + result.set(true); + } else { + btcPeersListener = (observable, oldValue, newValue) -> { + if (walletsSetup.hasSufficientPeersForBroadcast()) { + walletsSetup.numPeersProperty().removeListener(btcPeersListener); + result.set(true); + } + }; + walletsSetup.numPeersProperty().addListener(btcPeersListener); } + return result; + } + + private BooleanProperty isP2pBootstrapped() { + BooleanProperty result = new SimpleBooleanProperty(); + if (p2PService.isBootstrapped()) { + result.set(true); + } else { + bootstrapListener = new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + p2PService.removeP2PServiceListener(bootstrapListener); + result.set(true); + } + }; + p2PService.addP2PServiceListener(bootstrapListener); + } + return result; + } - return checkNotNull(trade.getContract()).getSellerPaymentAccountPayload() instanceof AssetsAccountPayload; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Validation + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isXmrTrade(Trade trade) { + return (checkNotNull(trade.getOffer()).getCurrencyCode().equals("XMR")); + } + + private boolean isExpectedTradeState(Trade.State newValue) { + return newValue == Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG; } private boolean is32BitHexStringInValid(String hexString) { @@ -192,12 +352,6 @@ private boolean is32BitHexStringInValid(String hexString) { return false; } - private boolean networkAndWalletReady() { - return p2PService.isBootstrapped() && - walletsSetup.isDownloadComplete() && - walletsSetup.hasSufficientPeersForBroadcast(); - } - private boolean isAutoConfDisabledByFilter() { return filterManager.getFilter() != null && filterManager.getFilter().isDisableAutoConf(); diff --git a/core/src/main/java/bisq/core/user/AutoConfirmSettings.java b/core/src/main/java/bisq/core/user/AutoConfirmSettings.java index 81fb2a57058..f94e0538492 100644 --- a/core/src/main/java/bisq/core/user/AutoConfirmSettings.java +++ b/core/src/main/java/bisq/core/user/AutoConfirmSettings.java @@ -25,10 +25,13 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Getter public final class AutoConfirmSettings implements PersistablePayload { public interface Listener { @@ -42,13 +45,21 @@ public interface Listener { private String currencyCode; private List listeners = new CopyOnWriteArrayList<>(); - static AutoConfirmSettings getDefaultForXmr(List serviceAddresses) { - return new AutoConfirmSettings( - false, - 5, - Coin.COIN.value, - serviceAddresses, - "XMR"); + @SuppressWarnings("SameParameterValue") + static Optional getDefault(List serviceAddresses, String currencyCode) { + //noinspection SwitchStatementWithTooFewBranches + switch (currencyCode) { + case "XMR": + return Optional.of(new AutoConfirmSettings( + false, + 5, + Coin.COIN.value, + serviceAddresses, + "XMR")); + default: + log.error("No AutoConfirmSettings supported yet for currency {}", currencyCode); + return Optional.empty(); + } } public AutoConfirmSettings(boolean enabled, diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index d267cfa604e..f04b4c6a5f3 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -33,7 +33,6 @@ import bisq.network.p2p.network.BridgeAddressProvider; -import bisq.common.app.DevEnv; import bisq.common.config.BaseCurrencyNetwork; import bisq.common.config.Config; import bisq.common.proto.persistable.PersistedDataHost; @@ -116,24 +115,24 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid )); public static final ArrayList BSQ_MAIN_NET_EXPLORERS = new ArrayList<>(Arrays.asList( - new BlockChainExplorer("bsq.ninja (@wiz)", "https://bsq.ninja/tx.html?tx=", "https://bsq.ninja/Address.html?addr="), - new BlockChainExplorer("bsq.sqrrm.net (@sqrrm)", "https://bsq.sqrrm.net/tx.html?tx=", "https://bsq.sqrrm.net/Address.html?addr="), - new BlockChainExplorer("bsq.bisq.services (@devinbileck)", "https://bsq.bisq.services/tx.html?tx=", "https://bsq.bisq.services/Address.html?addr="), + new BlockChainExplorer("mempool.space (@wiz)", "https://mempool.space/bisq/tx/", "https://mempool.space/bisq/address/"), + new BlockChainExplorer("mempool.emzy.de (@emzy)", "https://mempool.emzy.de/bisq/tx/", "https://mempool.emzy.de/bisq/address/"), + new BlockChainExplorer("mempool.bisq.services (@devinbileck)", "https://mempool.bisq.services/bisq/tx/", "https://mempool.bisq.services/bisq/address/"), new BlockChainExplorer("bsq.vante.me (@mrosseel)", "https://bsq.vante.me/tx.html?tx=", "https://bsq.vante.me/Address.html?addr="), - new BlockChainExplorer("bsq.emzy.de (@emzy)", "https://bsq.emzy.de/tx.html?tx=", "https://bsq.emzy.de/Address.html?addr="), + new BlockChainExplorer("bsq.sqrrm.net (@sqrrm)", "https://bsq.sqrrm.net/tx.html?tx=", "https://bsq.sqrrm.net/Address.html?addr="), new BlockChainExplorer("bsq.bisq.cc (@m52go)", "https://bsq.bisq.cc/tx.html?tx=", "https://bsq.bisq.cc/Address.html?addr=") )); - // list of XMR proof providers : this list will be used if no preference has been set - public static final List getDefaultXmrProofProviders() { - if (DevEnv.isDevMode()) { - return new ArrayList<>(Arrays.asList("78.47.61.90:8081")); - } else { - // TODO we need at least 2 for release - return new ArrayList<>(Arrays.asList( - "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion")); - } - } + private static final ArrayList XMR_TX_PROOF_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( + "xmrblocks.monero.emzy.de", // @emzy + "explorer.monero.wiz.biz", // @wiz + "xmrblocks.bisq.services" // @devinbileck + )); + private static final ArrayList XMR_TX_PROOF_SERVICES = new ArrayList<>(Arrays.asList( + "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion", // @emzy + "wizxmr4hbdxdszqm5rfyqvceyca5jq62ppvtuznasnk66wvhhvgm3uyd.onion", // @wiz + "devinxmrwu4jrfq2zmq5kqjpxb44hx7i7didebkwrtvmvygj4uuop2ad.onion" // @devinbileck + )); public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; @@ -321,7 +320,11 @@ public void readPersisted() { } if (prefPayload.getAutoConfirmSettingsList().isEmpty()) { - getAutoConfirmSettingsList().add(AutoConfirmSettings.getDefaultForXmr(getDefaultXmrProofProviders())); + List defaultXmrTxProofServices = getDefaultXmrTxProofServices(); + AutoConfirmSettings.getDefault(defaultXmrTxProofServices, "XMR") + .ifPresent(xmrAutoConfirmSettings -> { + getAutoConfirmSettingsList().add(xmrAutoConfirmSettings); + }); } // We set the capability in CoreNetworkCapabilities if the program argument is set. @@ -332,7 +335,6 @@ public void readPersisted() { persist(); } - /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -875,6 +877,14 @@ public int getBlockNotifyPort() { } } + public List getDefaultXmrTxProofServices() { + if (config.useLocalhostForP2P) { + return XMR_TX_PROOF_SERVICES_CLEAR_NET; + } else { + return XMR_TX_PROOF_SERVICES; + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index fbf97ea575f..3af5ecfa86d 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -52,9 +52,7 @@ public final class PreferencesPayload implements UserThreadMappedPersistableEnvelope { private String userLanguage; private Country userCountry; - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private List fiatCurrencies = new ArrayList<>(); - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private List cryptoCurrencies = new ArrayList<>(); private BlockChainExplorer blockChainExplorerMainNet; private BlockChainExplorer blockChainExplorerTestNet; diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index 0f6957c83ee..d9adac9a09c 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -24,7 +24,6 @@ import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.RevolutAccount; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.refund.refundagent.RefundAgent; @@ -51,7 +50,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; import java.util.stream.Collectors; import lombok.AllArgsConstructor; @@ -128,16 +126,6 @@ public void persist() { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void onAllServicesInitialized(@Nullable Consumer> resultHandler) { - if (resultHandler != null) { - resultHandler.accept(paymentAccountsAsObservable.stream() - .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) - .map(paymentAccount -> (RevolutAccount) paymentAccount) - .filter(RevolutAccount::userNameNotSet) - .collect(Collectors.toList())); - } - } - @Nullable public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); @@ -202,6 +190,8 @@ public boolean hasPaymentAccountForCurrency(TradeCurrency tradeCurrency) { /////////////////////////////////////////////////////////////////////////////////////////// public void addPaymentAccount(PaymentAccount paymentAccount) { + paymentAccount.onAddToUser(); + boolean changed = paymentAccountsAsObservable.add(paymentAccount); setCurrentPaymentAccount(paymentAccount); if (changed) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 1474fb0628e..b44aa54d5ca 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -572,9 +572,9 @@ portfolio.pending.step5.completed=Completed portfolio.pending.step3_seller.autoConf.status.label=Auto-confirm status portfolio.pending.autoConf=Auto-confirmed - +portfolio.pending.autoConf.blocks=XMR confirmations: {0} / Required: {1} portfolio.pending.autoConf.state.xmr.txKeyReused=Transaction key re-used. Please open a dispute. -portfolio.pending.autoConf.state.confirmations=Confirmations: {0}/{1} +portfolio.pending.autoConf.state.confirmations=XMR confirmations: {0}/{1} portfolio.pending.autoConf.state.txNotFound=Transaction not seen in mem-pool yet portfolio.pending.autoConf.state.txKeyOrTxIdInvalid=No valid transaction ID / transaction key portfolio.pending.autoConf.state.filterDisabledFeature=Disabled by developers. @@ -588,15 +588,17 @@ portfolio.pending.autoConf.state.INVALID_DATA=Peer provided invalid data. {0} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.PAYOUT_TX_ALREADY_PUBLISHED=Payout transaction was already published. # suppress inspection "UnusedProperty" -portfolio.pending.autoConf.state.REQUESTS_STARTED=Proof requests started +portfolio.pending.autoConf.state.DISPUTE_OPENED=Dispute was opened. Auto-confirm is deactivated for that trade. +# suppress inspection "UnusedProperty" +portfolio.pending.autoConf.state.REQUESTS_STARTED=Transaction proof requests started # suppress inspection "UnusedProperty" -portfolio.pending.autoConf.state.PENDING=Service results: {0}/{1}; {2} +portfolio.pending.autoConf.state.PENDING=Success results: {0}/{1}; {2} # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.COMPLETED=Proof at all services succeeded # suppress inspection "UnusedProperty" -portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. +portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. No auto-confirm possible. # suppress inspection "UnusedProperty" -portfolio.pending.autoConf.state.FAILED=A service returned with a failure. +portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. portfolio.pending.step1.info=Deposit transaction has been published.\n{0} need to wait for at least one blockchain confirmation before starting the payment. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. @@ -635,6 +637,7 @@ portfolio.pending.step2_buyer.bank=Please go to your online banking web page and # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Please contact the BTC seller by the provided contact and arrange a meeting to pay {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer portfolio.pending.step2_buyer.sellersAddress=Seller''s {0} address portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used @@ -753,7 +756,7 @@ portfolio.pending.step3_seller.buyersAddress=Buyers {0} address portfolio.pending.step3_seller.yourAccount=Your trading account portfolio.pending.step3_seller.xmrTxHash=Transaction ID portfolio.pending.step3_seller.xmrTxKey=Transaction key -portfolio.pending.step3_seller.buyersAccount=Buyers trading account +portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain confirmations at your altcoin wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations. @@ -1087,15 +1090,15 @@ settings.tab.network=Network info settings.tab.about=About setting.preferences.general=General preferences -setting.preferences.explorer=Bitcoin block explorer -setting.preferences.explorer.bsq=BSQ block explorer +setting.preferences.explorer=Bitcoin Explorer +setting.preferences.explorer.bsq=Bisq Explorer setting.preferences.deviation=Max. deviation from market price setting.preferences.avoidStandbyMode=Avoid standby mode setting.preferences.autoConfirmXMR=XMR auto-confirm setting.preferences.autoConfirmEnabled=Enabled setting.preferences.autoConfirmRequiredConfirmations=Required confirmations setting.preferences.autoConfirmMaxTradeSize=Max. trade amount (BTC) -setting.preferences.autoConfirmServiceAddresses=Service addresses +setting.preferences.autoConfirmServiceAddresses=Monero Explorer URLs (uses Tor, except for localhost, LAN IP addresses, and *.local hostnames) setting.preferences.deviationToLarge=Values higher than {0}% are not allowed. setting.preferences.txFee=Withdrawal transaction fee (satoshis/byte) setting.preferences.useCustomValue=Use custom value @@ -1245,9 +1248,6 @@ setting.about.shortcuts.openEmergencyBsqWalletTool=Open emergency wallet tool fo setting.about.shortcuts.showTorLogs=Toggle log level for Tor messages between DEBUG and WARN -setting.about.shortcuts.showDisputeStatistics=Show summary of all disputes -setting.about.shortcuts.showDisputeStatistics.value=Navigate to disputes view and press: {0} - setting.about.shortcuts.manualPayoutTxWindow=Open window for manual payout from 2of2 Multisig deposit tx setting.about.shortcuts.reRepublishAllGovernanceData=Republish DAO governance data (proposals, votes) @@ -1261,9 +1261,6 @@ setting.about.shortcuts.registerArbitrator.value=Navigate to account and press: setting.about.shortcuts.registerMediator=Register mediator (mediator/arbitrator only) setting.about.shortcuts.registerMediator.value=Navigate to account and press: {0} -setting.about.shortcuts.reOpenDispute=Re-open already closed dispute (mediator/arbitrator only) -setting.about.shortcuts.reOpenDispute.value=Select closed dispute and press: {0} - setting.about.shortcuts.openSignPaymentAccountsWindow=Open window for account age signing (legacy arbitrators only) setting.about.shortcuts.openSignPaymentAccountsWindow.value=Navigate to legacy arbitrator view and press: {0} @@ -1272,7 +1269,7 @@ setting.about.shortcuts.sendAlertMsg=Send alert or update message (privileged ac setting.about.shortcuts.sendFilter=Set Filter (privileged activity) setting.about.shortcuts.sendPrivateNotification=Send private notification to peer (privileged activity) -setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar or dispute and press: {0} +setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar and press: {0} setting.info.headline=New XMR auto-confirm Feature setting.info.msg=When selling BTC for XMR you can use the auto-confirm feature to verify that the correct amount of \ @@ -2554,8 +2551,9 @@ showWalletDataWindow.walletData=Wallet data showWalletDataWindow.includePrivKeys=Include private keys setXMRTxKeyWindow.headline=Prove sending of XMR -setXMRTxKeyWindow.txHash=Transaction ID -setXMRTxKeyWindow.txKey=Transaction key +setXMRTxKeyWindow.note=Adding tx info below enables auto-confirm for quicker trades. See more: https://bisq.wiki/Trading_Monero +setXMRTxKeyWindow.txHash=Transaction ID (optional) +setXMRTxKeyWindow.txKey=Transaction key (optional) # We do not translate the tac because of the legal nature. We would need translations checked by lawyers # in each language which is too expensive atm. @@ -3048,6 +3046,7 @@ payment.account=Account payment.account.no=Account no. payment.account.name=Account name payment.account.userName=User name +payment.account.phoneNr=Phone number payment.account.owner=Account owner full name payment.account.fullName=Full name (first, middle, last) payment.account.state=State/Province/Region diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 6ddcb544d8b..6f0ceac2fca 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Vermittlung support.tab.legacyArbitration.support=Legacy-Vermittlung support.tab.ArbitratorsSupportTickets={0} Tickets -support.filter=Liste filtern +support.filter=Search disputes support.filter.prompt=Tragen sie Handel ID, Datum, Onion Adresse oder Kontodaten +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Private Benachrichtigung senden +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=Keine offenen Tickets vorhanden support.sendingMessage=Nachricht wird gesendet... support.receiverNotOnline=Empfänger ist nicht online. Nachricht wird in der Mailbox gespeichert. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=Sie haben um Mediation gebeten.\n\n{0}\n\nB support.peerOpenedTicket=Ihr Trading-Partner hat aufgrund technischer Probleme Unterstützung angefordert.\n\n{0}\n\nBisq-Version: {1} support.peerOpenedDispute=Ihr Trading-Partner hat einen Konflikt eröffnet.\n\n{0}\n\nBisq-Version: {1} support.peerOpenedDisputeForMediation=Ihr Trading-Partner hat eine Mediation beantragt.\n\n{0}\n\nBisq-Version: {1} -support.mediatorsDisputeSummary=Systemmeldung:\nKonflikt-Zusammenfassung des Mediators:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Node-Adresse des Mediators: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Verbundene Peers settings.net.onionAddressColumn=Onion-Adresse settings.net.creationDateColumn=Eingerichtet settings.net.connectionTypeColumn=Ein/Aus -settings.net.totalTrafficLabel=Gesamter Verkehr +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Umlaufzeit settings.net.sentBytesColumn=Gesendet settings.net.receivedBytesColumn=Erhalten @@ -989,7 +995,8 @@ settings.net.heightColumn=Höhe settings.net.needRestart=Sie müssen die Anwendung neustarten, um die Änderungen anzuwenden.\nMöchten Sie jetzt neustarten? settings.net.notKnownYet=Noch nicht bekannt... -settings.net.sentReceived=Gesendet: {0}, erhalten: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[IP Adresse:Port | Hostname:Port | Onion-Adresse:Port] (Komma getrennt). Port kann weggelassen werden, wenn Standard genutzt wird (8333). settings.net.seedNode=Seed-Knoten settings.net.directPeer=Peer (direkt) @@ -1011,8 +1018,8 @@ setting.about.support=Bisq unterstützen setting.about.def=Bisq ist keine Firma, sondern ein Gemeinschaftsprojekt, das offen für Mitwirken ist. Wenn Sie an Bisq mitwirken oder das Projekt unterstützen wollen, folgen Sie bitte den unten stehenden Links. setting.about.contribute=Mitwirken setting.about.providers=Datenanbieter -setting.about.apisWithFee=Bisq nutzt für Fiatgeld- und Altcoin-Marktpreise sowie geschätzte Mining-Gebühren die APIs 3tr. -setting.about.apis=Bisq nutzt für Fiatgeld- und Altcoin-Marktpreise die APIs 3tr. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Marktpreise zur Verfügung gestellt von setting.about.feeEstimation.label=Geschätzte Mining-Gebühr bereitgestellt von setting.about.versionDetails=Versionsdetails @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Öffnen Sie die Peer-Info account.tab.arbitratorRegistration=Vermittler-Registrierung account.tab.mediatorRegistration=Mediator-Registrierung account.tab.refundAgentRegistration=Registrierung des Rückerstattungsbeauftragten +account.tab.signing=Signing account.tab.account=Konto account.info.headline=Willkommen in Ihrem Bisq-Konto account.info.msg=Hier können Sie Trading-Konten für nationale Währungen und Altcoins hinzufügen und Backups für Ihre Wallets & Kontodaten erstellen.\n\nEine leere Bitcoin-Wallet wurde erstellt, als Sie das erste Mal Bisq gestartet haben.\n\nWir empfehlen, dass Sie Ihre Bitcoin-Wallet-Seed-Wörter aufschreiben (siehe Tab oben) und sich überlegen ein Passwort hinzuzufügen, bevor Sie einzahlen. Bitcoin-Einzahlungen und Auszahlungen werden unter \"Gelder\" verwaltet.\n\nHinweis zu Privatsphäre & Sicherheit: da Bisq eine dezentralisierte Börse ist, bedeutet dies, dass all Ihre Daten auf ihrem Computer bleiben. Es gibt keine Server und wir haben keinen Zugriff auf Ihre persönlichen Informationen, Ihre Gelder oder selbst Ihre IP-Adresse. Daten wie Bankkontonummern, Altcoin- & Bitcoinadressen, etc werden nur mit Ihrem Trading-Partner geteilt, um Trades abzuschließen, die Sie initiiert haben (im Falle eines Konflikts wird der Vermittler die selben Daten sehen wie Ihr Trading-Partner). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Auszahlungsbetrag des Käufers disputeSummaryWindow.payoutAmount.seller=Auszahlungsbetrag des Verkäufers disputeSummaryWindow.payoutAmount.invert=Verlierer als Veröffentlicher nutzen disputeSummaryWindow.reason=Grund des Konflikts -disputeSummaryWindow.reason.bug=Fehler -disputeSummaryWindow.reason.usability=Nutzbarkeit -disputeSummaryWindow.reason.protocolViolation=Protokollverletzung -disputeSummaryWindow.reason.noReply=Keine Antwort -disputeSummaryWindow.reason.scam=Betrug -disputeSummaryWindow.reason.other=Andere -disputeSummaryWindow.reason.bank=Bank + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Fehler +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Nutzbarkeit +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Protokollverletzung +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Keine Antwort +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Betrug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Andere +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Bank +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Zusammenfassende Anmerkungen disputeSummaryWindow.addSummaryNotes=Zusammenfassende Anmerkungen hinzufügen disputeSummaryWindow.close.button=Ticket schließen -disputeSummaryWindow.close.msg=Ticket geschlossen am {0}\n\nZusammenfassung:\nAuszahlungsbetrag für BTC-Käufer: {1}\nAuszahlungsbetrag für BTC-Verkäufer: {2}\n\nZusammenfassende Hinweise:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNächste Schritte:\nTrade öffnen und Empfehlung des Mediators akzeptieren oder ablehnen disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNächste Schritte:\nEs sind keine weiteren Schritte Ihrerseits erforderlich. Wenn der Vermittler sich zu Ihren Gunsten entschieden hat, sehen Sie unter Fonds/Transaktionen eine Transaktion "Rückerstattung aus dem Vermittlungsverfahren" disputeSummaryWindow.close.closePeer=Sie müssen auch das Ticket des Handelspartners schließen! @@ -1985,7 +2013,8 @@ filterWindow.onions=Herausgefilterte Onion-Adressen (durch Kommas getrennt) filterWindow.accounts=Herausgefilterte Handelskonten Daten:\nFormat: Komma getrennte Liste von [Zahlungsmethoden ID | Datenfeld | Wert] filterWindow.bannedCurrencies=Herausgefilterte Währungscodes (durch Kommas getrennt) filterWindow.bannedPaymentMethods=Herausgefilterte Zahlungsmethoden-IDs (durch Kommas getrennt) -filterWindow.bannedSignerPubKeys=Gefilterte Unterzeichner-Pubkeys (Komma getr. Hex der Pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Gefilterte Vermittler (mit Komma getr. Onion-Adressen) filterWindow.mediators=Gefilterte Mediatoren (mit Komma getr. Onion-Adressen) filterWindow.refundAgents=Gefilterte Rückerstattungsagenten (mit Komma getr. Onion-Adressen) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=Sie haben nicht genügend BTC-Gelder, popup.warning.bsqChangeBelowDustException=Diese Transaktion erzeugt eine BSQ-Wechselgeld-Ausgabe, die unter dem Dust-Limit (5.46 BSQ) liegt und vom Bitcoin-Netzwerk abgelehnt werden würde.\n\nSie müssen entweder einen höheren Betrag senden, um die Wechselgeld-Ausgabe zu vermeiden (z.B. indem Sie den Dust-Betrag zu Ihrem Sende-Betrag hinzufügen) oder mehr BSQ-Guthaben zu Ihrer Wallet hinzufügen, damit Sie vermeiden, eine Dust-Ausgabe zu generieren.\n\nDie Dust-Ausgabe ist {0}. popup.warning.btcChangeBelowDustException=Diese Transaktion erzeugt eine Wechselgeld-Ausgabe, die unter dem Dust-Limit (546 Satoshi) liegt und vom Bitcoin-Netzwerk abgelehnt würde.\n\nSie müssen den Dust-Betrag zu Ihrem Sende-Betrag hinzufügen, um zu vermeiden, dass eine Dust-Ausgabe generiert wird.\n\nDie Dust-Ausgabe ist {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=Sie haben nicht genügend BSQ-Gelder, um die Handels-Gebühr in BSQ zu bezahlen.\nSie können die Gebühr in BTC bezahlen, oder müssen Ihre BSQ-Wallet füllen. Sie können BSQ in Bisq kaufen.\n\nFehlende BSQ Gelder: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=Ihre BSQ-Wallet hat keine ausreichenden Gelder, um die Handels-Gebühr in BSQ zu bezahlen. popup.warning.messageTooLong=Ihre Nachricht überschreitet die maximal erlaubte Größe. Sende Sie diese in mehreren Teilen oder laden Sie sie in einen Dienst wie https://pastebin.com hoch. popup.warning.lockedUpFunds=Sie haben gesperrtes Guthaben aus einem gescheiterten Trade.\nGesperrtes Guthaben: {0} \nEinzahlungs-Tx-Adresse: {1}\nTrade ID: {2}.\n\nBitte öffnen Sie ein Support-Ticket, indem Sie den Trade im Bildschirm "Offene Trades" auswählen und auf \"alt + o\" oder \"option + o\" drücken. @@ -2437,6 +2466,7 @@ seed.restore.error=Beim Wiederherstellen der Wallets mit den Seed-Wörtern ist e payment.account=Konto payment.account.no=Kontonummer payment.account.name=Kontoname +payment.account.userName=User name payment.account.owner=Vollständiger Name des Kontoinhabers payment.account.fullName=Vollständiger Name (vor, zweit, nach) payment.account.state=Bundesland/Landkreis/Region @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=E-Mail oder Telefonnummer payment.venmo.venmoUserName=Venmo Nutzername payment.popmoney.accountId=E-Mail oder Telefonnummer -payment.revolut.email=E-Mail -payment.revolut.phoneNr=Registrierte Telefonnummer payment.promptPay.promptPayId=Personalausweis/Steuernummer oder Telefonnr. payment.supportedCurrencies=Unterstützte Währungen payment.limitations=Einschränkungen @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=Um das Rückbuchungsrisiko zu begrenzen, setzt B payment.cashDeposit.info=Bitte bestätigen Sie, dass Ihre Bank Bareinzahlungen in Konten von anderen Personen erlaubt. Zum Beispiel werden diese Einzahlungen bei der Bank of America und Wells Fargo nicht mehr erlaubt. -payment.revolut.info=Bitte vergewissern sie sich, dass die Telefonnummer die sie verwendet haben auch bei Revolut registriert ist. Ansonsten kann der BTC-Käufer ihnen den Betrag nicht überweisen. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Zahlungsanweisungen sind eine der privateren Fiat-Kaufmethoden, die auf Bisq verfügbar sind.\n\nBitte beachten Sie jedoch die mit der Verwendung verbundenen potenziell erhöhten Risiken. Bisq trägt keine Verantwortung für den Fall, dass eine gesendete Zahlungsanweisung gestohlen wird, und der Mediator oder Vermittler wird in solchen Fällen die BTC an den Absender der Zahlungsanweisung vergeben, sofern er Trackinginformationen und Belege vorlegen kann. Es kann für den Absender ratsam sein, den Namen des BTC-Verkäufers auf die Zahlungsanweisung zu schreiben, um das Risiko zu minimieren, dass die Zahlungsanweisung von jemand anderem eingelöst wird. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=Kontaktinformationen payment.f2f.contact.prompt=Wie möchten Sie vom Handeslpartner kontaktiert werden? (E-Mailadresse, Telefonnummer,...) diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 8b689b2c109..0ee316d1a03 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediación support.tab.arbitration.support=Arbitraje support.tab.legacyArbitration.support=Legado de arbitraje support.tab.ArbitratorsSupportTickets=Tickets de {0} -support.filter=Filtrar lista +support.filter=Search disputes support.filter.prompt=Introduzca ID de transacción, fecha, dirección onion o datos de cuenta. +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Enviar notificación privada +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=No hay tickets abiertos support.sendingMessage=Enviando mensaje... support.receiverNotOnline=El receptor no está conectado. El mensaje se ha guardado en su bandeja de entrada. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=Ha solicitado mediación\n\n{0}\n\nVersión support.peerOpenedTicket=Su par de intercambio ha solicitado soporte debido a problemas técnicos\n\n{0}\n\nVersión Bisq: {1} support.peerOpenedDispute=Su pareja de intercambio ha solicitado una disputa.\n\n{0}\n\nVersión Bisq: {1} support.peerOpenedDisputeForMediation=Su par de intercambio ha solicitado mediación.\n\n{0}\n\nVersión Bisq: {1} -support.mediatorsDisputeSummary=Sistema de mensaje:\nResumen de disputa del mediador:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Dirección del nodo del mediador: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Pares conectados settings.net.onionAddressColumn=Dirección onion settings.net.creationDateColumn=Establecido settings.net.connectionTypeColumn=Dentro/Fuera -settings.net.totalTrafficLabel=Tráfico total +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Tiempo de ida y vuelta settings.net.sentBytesColumn=Enviado settings.net.receivedBytesColumn=Recibido @@ -989,7 +995,8 @@ settings.net.heightColumn=Altura settings.net.needRestart=Necesita reiniciar la aplicación para aplicar ese cambio.\n¿Quiere hacerlo ahora? settings.net.notKnownYet=Aún no conocido... -settings.net.sentReceived=Enviado: {0}, recibido: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[Dirección IP:puerto | host:puerto | dirección onion:puerto] (separado por coma). El puerto puede ser omitido si se utiliza el predeterminado (8333). settings.net.seedNode=Nodo semilla settings.net.directPeer=Par (directo) @@ -1011,8 +1018,8 @@ setting.about.support=Apoye a Bisq setting.about.def=Bisq no es una compañía - es un proyecto abierto a la comunidad. Si quiere participar o ayudar a Bisq por favor siga los enlaces de abajo. setting.about.contribute=Contribuir setting.about.providers=Proveedores de datos -setting.about.apisWithFee=Bisq usa APIs de terceros para los precios de los mercados Fiat y Altcoin así como para la estimación de tasas de minado. -setting.about.apis=Bisq utiliza APIs de terceros para los precios de mercado de Fiat y Altcoin. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Precios de mercado proporcionados por: setting.about.feeEstimation.label=Estimación de comisión de minería proporcionada por: setting.about.versionDetails=Detalles de la versión @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Abrir información de par account.tab.arbitratorRegistration=Registro de árbitro account.tab.mediatorRegistration=Registro de mediador account.tab.refundAgentRegistration=Registro de agente de devolución de fondos +account.tab.signing=Signing account.tab.account=Cuenta account.info.headline=Bienvenido a su cuenta Bisq account.info.msg=Aquí puede añadir cuentas de intercambio para monedas nacionales y altcoins y crear una copia de su cartera y datos de cuenta.\n\nUna nueva cartera Bitcoin fue creada la primera vez que inició Bisq.\n\nRecomendamos encarecidamente que escriba sus palabras de la semilla de la cartera Bitcoin (mire pestaña arriba) y considere añadir una contraseña antes de enviar fondos. Los ingresos y retiros de Bitcoin se administran en la sección \"Fondos\".\n\nNota de privacidad y seguridad: Debido a que Bisq es un exchange descentralizado, todos sus datos se guardan en su ordenador. No hay servidores, así que no tenemos acceso a su información personal, sus saldos, o incluso su dirección IP. Datos como número de cuenta bancaria, direcciones altcoin y Bitcoin, etc son solo compartidos con su par de intercambio para completar intercambios iniciados (en caso de una disputa el mediador o árbitro verá los mismos datos que el par de intercambio). @@ -1151,7 +1159,7 @@ account.password.info=Con protección por contraseña necesitará introducir su account.seed.backup.title=Copia de seguridad de palabras semilla del monedero account.seed.info=Por favor apunte en un papel tanto las palabras semilla del monedero como la fecha! Puede recuperar su monedero en cualquier momento con las palabras semilla y la fecha.\nLas mismas palabras semilla se usan para el monedero BTC como BSQ\n\nDebe apuntar las palabras semillas en una hoja de papel. No la guarde en su computadora.\n\nPor favor, tenga en cuenta que las palabras semilla no son un sustituto de la copia de seguridad.\nNecesita hacer la copia de seguridad de todo el directorio de aplicación en la pantalla \"Cuenta/Copia de Seguridad\" para recuperar un estado de aplicación válido y los datos.\nImportar las palabras semilla solo se recomienda para casos de emergencia. La aplicación no será funcional sin una buena copia de seguridad de los archivos de la base de datos y las claves! -account.seed.backup.warning=Please note that the seed words are NOT a replacement for a backup.\nYou need to create a backup of the whole application directory from the \"Account/Backup\" screen to recover application state and data.\nImporting seed words is only recommended for emergency cases. The application will not be functional without a proper backup of the database files and keys!\n\nSee the wiki page https://bisq.wiki/Backing_up_application_data for extended info. +account.seed.backup.warning=Por favor tenga en cuenta que las palabras semilla NO SON un sustituto de la copia de seguridad.\nTiene que crear una copia de todo el directorio de aplicación desde la pantalla \"Cuenta/Copia de seguridad\ para recuperar el estado y datos de la aplicación.\nImportar las palabras semilla solo se recomienda para casos de emergencia. La aplicación no funcionará sin una copia de seguridad adecuada de las llaves y archivos de sistema!\n\nVea la página wiki https://bisq.wiki/Backing_up_application_data para más información. account.seed.warn.noPw.msg=No ha establecido una contraseña de cartera que proteja la visualización de las palabras semilla.\n\n¿Quiere que se muestren las palabras semilla? account.seed.warn.noPw.yes=Sí, y no preguntar de nuevo account.seed.enterPw=Introducir contraseña para ver las palabras semilla @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Cantidad de pago del comprador disputeSummaryWindow.payoutAmount.seller=Cantidad de pago del vendedor disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador disputeSummaryWindow.reason=Razón de la disputa -disputeSummaryWindow.reason.bug=Bug -disputeSummaryWindow.reason.usability=Usabilidad -disputeSummaryWindow.reason.protocolViolation=Violación del protocolo -disputeSummaryWindow.reason.noReply=Sin respuesta -disputeSummaryWindow.reason.scam=Estafa -disputeSummaryWindow.reason.other=Otro -disputeSummaryWindow.reason.bank=Banco + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilidad +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violación del protocolo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Sin respuesta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Estafa +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Otro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banco +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Nota de resumen disputeSummaryWindow.addSummaryNotes=Añadir notas de sumario disputeSummaryWindow.close.button=Cerrar ticket -disputeSummaryWindow.close.msg=Ticket cerrado en {0}\n\nSumario:\nCantidad de pago para comprador de BTC: {1}\nCantidad de pago para vendedor de BTC: {2}\n\nNotas del sumario:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nSiguientes pasos:\nAbrir intercambio y aceptar o rechazar la sugerencia del mediador disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nSiguientes pasos:\nNo se requiere ninguna acción adicional. Si el árbitro decide en su favor, verá una transacción de "Devolución de fondos de arbitraje" en Fondos/Transacciones disputeSummaryWindow.close.closePeer=Necesitar cerrar también el ticket del par de intercambio! @@ -1985,7 +2013,8 @@ filterWindow.onions=Direcciones onion filtradas (separadas por coma) filterWindow.accounts=Cuentas de intercambio filtradas:\nFormato: lista de [ID método de pago | campo de datos | valor] separada por coma. filterWindow.bannedCurrencies=Códigos de moneda filtrados (separados por coma) filterWindow.bannedPaymentMethods=ID's de métodos de pago filtrados (separados por coma) -filterWindow.bannedSignerPubKeys=Llaves públicas de los firmantes filtrados (separados por coma, hex de las llaves públicas)\n +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Árbitros filtrados (direcciones onion separadas por coma) filterWindow.mediators=Mediadores filtrados (direcciones onion separadas por coma) filterWindow.refundAgents=Agentes de devolución de fondos filtrados (direcciones onion separadas por coma) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=No tiene suficientes fondos BTC para popup.warning.bsqChangeBelowDustException=Esta transacción crea un output BSQ de cambio que está por debajo del límite dust (5.46 BSQ) y sería rechazado por la red Bitcoin.\n\nTiene que enviar una cantidad mayor para evitar el output de cambio (Ej. agregando la cantidad de dust a su cantidad de envío) o añadir más fondos BSQ a su cartera para evitar generar un output de dust.\n\nEl output dust es {0}. popup.warning.btcChangeBelowDustException=Esta transacción crea un output de cambio que está por debajo del límite de dust (546 Satoshi) y sería rechazada por la red Bitcoin.\n\nDebe agregar la cantidad de dust a su cantidad de envío para evitar generar un output de dust.\n\nEl output de dust es {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=No tiene suficientes fondos BSQ para pagar la comisión de intercambio en BSQ. Puede pagar la tasa en BTC o tiene que fondear su monedero BSQ. Puede comprar BSQ en Bisq.\n\nFondos BSQ necesarios: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=Su monedero BSQ no tiene suficientes fondos para pagar la comisión de intercambio en BSQ. popup.warning.messageTooLong=Su mensaje excede el tamaño máximo permitido. Por favor, envíelo por partes o súbalo a un servicio como https://pastebin.com popup.warning.lockedUpFunds=Ha bloqueado fondos de un intercambio fallido.\nBalance bloqueado: {0}\nDirección de depósito TX: {1}\nID de intercambio: {2}.\n\nPor favor, abra un ticket de soporte seleccionando el intercambio en la pantalla de intercambios pendientes y haciendo clic en \"alt + o\" o \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=Un error ocurrió el restaurar los monederos con las palabras payment.account=Cuenta payment.account.no=Número de cuenta payment.account.name=Nombre de cuenta +payment.account.userName=User name payment.account.owner=Nombre completo del propietario de la cuenta payment.account.fullName=Nombre completo payment.account.state=Estado/Provincia/Región @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Correo electrónico o núm. de telefóno payment.venmo.venmoUserName=Nombre de usuario Venmo payment.popmoney.accountId=Correo electrónico o núm. de telefóno -payment.revolut.email=Email -payment.revolut.phoneNr=Número de teléfono registrado payment.promptPay.promptPayId=Citizen ID/Tax ID o número de teléfono payment.supportedCurrencies=Monedas soportadas payment.limitations=Límitaciones: @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=Para limitar el riesgo de devolución de cargo, payment.cashDeposit.info=Por favor confirme que su banco permite enviar depósitos de efectivo a cuentas de otras personas. Por ejemplo, Bank of America y Wells Fargo ya no permiten estos depósitos. -payment.revolut.info=Por favor asegúrese de que el número de teléfono que ha usado para la cuenta Revolut está registrada en Revolut, de lo contrario el comprador BTC no podrá enviarle los fondos. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Los giros postales son uno de los métodos de adquisición de fiat más privados disponibles en Bisq\n\nAún así, por favor tenga en cuenta el elevado riesgo potencial asociado a su uso. Bisq no contrae ninguna responsabilidad en caso de que el dinero enviado sea robado, y el mediador o árbitro en estos casos adjudiquen los BTC al emisor del giro postal, siempre que puedan mostrar recibos e información de seguimiento. Es aconsejable para el emisor escribir el nombre del emisor en el giro postal, para minimizar el riesgo de que el giro postal sea retirado por otra persona. +payment.usPostalMoneyOrder.info=Los intercambios usando US Postal Money Orders (USPMO) en Bisq requiere que entienda lo siguiente:\n\n- Los compradores de BTC deben escribir la dirección del vendedor en los campos de "Payer" y "Payee" y tomar una foto en alta resolución de la USPMO y del sobre con la prueba de seguimiento antes de enviar.\n- Los compradores de BTC deben enviar la USPMO con confirmación de entrega.\n\nEn caso de que sea necesaria la mediación, se requerirá al comprador que entregue las fotos al mediador o agente de devolución de fondos, junto con el número de serie de la USPMO, número de oficina postal, y la cantidad de USD, para que puedan verificar los detalles en la web de US Post Office.\n\nNo entregar la información requerida al Mediador o Árbitro resultará en pérdida del caso de disputa. \n\nEn todos los casos de disputa, el emisor de la USPMO tiene el 100% de responsabilidad en aportar la evidencia al Mediador o Árbitro.\n\nSi no entiende estos requerimientos, no comercie usando USPMO en Bisq. payment.f2f.contact=Información de contacto payment.f2f.contact.prompt=¿Cómo quiere ser contactado por el par de intercambio? (dirección email, número de teléfono...) diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index f27daa08595..eab54e76b1f 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=لیست فیلتر +support.filter=Search disputes support.filter.prompt=Enter trade ID, date, onion address or account data +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=ارسال اطلاع رسانی خصوصی +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=هیچ تیکتی به صورت باز وجود ندارد support.sendingMessage=در حال ارسال پیام ... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=همتایان متصل settings.net.onionAddressColumn=آدرس Onion settings.net.creationDateColumn=تثبیت شده settings.net.connectionTypeColumn=درون/بیرون -settings.net.totalTrafficLabel=مجموع ترافیک +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=تاخیر چرخشی settings.net.sentBytesColumn=ارسال شده settings.net.receivedBytesColumn=دریافت شده @@ -989,7 +995,8 @@ settings.net.heightColumn=Height settings.net.needRestart=به منظور اعمال آن تغییر باید برنامه را مجدداً راه اندازی کنید.\nآیا می‌خواهید این کار را هم اکنون انجام دهید؟ settings.net.notKnownYet=هنوز شناخته شده نیست ... -settings.net.sentReceived=ارسال شده: {0}، دریافت شده: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[آدرس آی پی: پورت | نام میزبان: پورت | آدرس Onion : پورت] (جدا شده با ویرگول). اگر از پیش فرض (8333) استفاده می شود، پورت می تواند حذف شود. settings.net.seedNode=گره ی اصلی settings.net.directPeer=همتا (مستقیم) @@ -1011,8 +1018,8 @@ setting.about.support=پشتیبانی از Bisq setting.about.def=Bisq یک شرکت نیست — یک پروژه اجتماعی است و برای مشارکت آزاد است. اگر می‌خواهید در آن مشارکت کنید یا از آن حمایت نمایید، لینک‌‌های زیر را دنبال کنید. setting.about.contribute=مشارکت setting.about.providers=ارائه دهندگان داده -setting.about.apisWithFee=Bisq از APIهای شخص ثالث 3rd party برای قیمت های روز فیات و آلت کوین و همچنین برای برآورد هزینه تراکنش شبکه استفاده می کند. -setting.about.apis=Bisq از APIهای شخص ثالث 3rd party برای قیمت های روز فیات و آلت کوین استفاده می کند. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=قیمت‌های بازار ارائه شده توسط setting.about.feeEstimation.label=برآورد کارمزد استخراج ارائه شده توسط setting.about.versionDetails=جزئیات نسخه @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar o account.tab.arbitratorRegistration=ثبت نام داور account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing account.tab.account=حساب account.info.headline=به حساب Bisq خود خوش آمدید account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=مقدار پرداختی خریدار disputeSummaryWindow.payoutAmount.seller=مقدار پرداختی فروشنده disputeSummaryWindow.payoutAmount.invert=استفاده از بازنده به عنوان منتشر کننده disputeSummaryWindow.reason=دلیل اختلاف -disputeSummaryWindow.reason.bug=اشکال -disputeSummaryWindow.reason.usability=قابلیت استفاده -disputeSummaryWindow.reason.protocolViolation=نقض پروتکل -disputeSummaryWindow.reason.noReply=بدون پاسخ -disputeSummaryWindow.reason.scam=کلاهبرداری -disputeSummaryWindow.reason.other=سایر -disputeSummaryWindow.reason.bank=بانک + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=اشکال +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=قابلیت استفاده +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=نقض پروتکل +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=بدون پاسخ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=کلاهبرداری +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=سایر +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=بانک +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=نکات خلاصه disputeSummaryWindow.addSummaryNotes=افزودن نکات خلاصه disputeSummaryWindow.close.button=بستن تیکت -disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nSummary notes:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=شما باید همچنین تیکت همتایان معامله را هم ببندید! @@ -1985,7 +2013,8 @@ filterWindow.onions=آدرس های Onion فیلتر شده (جدا شده با filterWindow.accounts=داده های حساب معاملاتی فیلترشده:\nفرمت: لیست جدا شده با ویرگول [شناسه روش پرداخت، زمینه داده، ارزش] filterWindow.bannedCurrencies=کدهای ارز فیلترشده (جدا شده با ویرگول) filterWindow.bannedPaymentMethods=شناسه‌های روش پرداخت فیلتر شده (جدا شده با ویرگول) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=داوران فیلتر شده (آدرس های Onion جدا شده با ویرگول) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=شما BTC کافی برای پردا popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=شما BTC کافی برای پرداخت کارمزد معامله آن تراکنش BSQ را ندارید. می‌توانید کارمزد را با BTC پرداخت کنید و یا اینکه کیف پول BSQ خود را شارژ کنید. می‌توانید از Bisq اقدام به خرید BSQ کنید.\n\nBSQ موردنیاز: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=کیف‌پول BSQ شما BSQ کافی برای پرداخت کارمزد معامله به BSQ را ندارد. popup.warning.messageTooLong=پیام شما بیش از حداکثر اندازه مجاز است. لطفا آن را در چند بخش ارسال کنید یا آن را در یک سرویس مانند https://pastebin.com آپلود کنید. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=هنگام بازگرداندن کیف پول با کلمات payment.account=حساب payment.account.no=شماره حساب payment.account.name=نام حساب +payment.account.userName=User name payment.account.owner=نام کامل مالک حساب payment.account.fullName=نام کامل (اول، وسط، آخر) payment.account.state=ایالت/استان/ناحیه @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=ایمیل یا شماره تلفن payment.venmo.venmoUserName=نام کاربری Venmo payment.popmoney.accountId=ایمیل یا شماره تلفن -payment.revolut.email=ایمیل -payment.revolut.phoneNr=Registered phone no. payment.promptPay.promptPayId=شناسه شهروندی/شناسه مالیاتی یا شماره تلفن payment.supportedCurrencies=ارزهای مورد حمایت payment.limitations=محدودیت‌ها @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=لطفا مطمئن شوید که بانک شما اجازه پرداخت سپرده نفد به حساب دیگر افراد را می‌دهد. برای مثال، Bank of America و Wells Fargo دیگر اجازه چنین پرداخت‌هایی را نمی‌دهند. -payment.revolut.info=Please be sure that the phone number you used for your Revolut account is registered at Revolut otherwise the BTC buyer cannot send you the funds. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Money orders are one of the more private fiat purchase methods available on Bisq.\n\nHowever, please be aware of potentially increased risks associated with their use. Bisq will not bear any responsibility in case a sent money order is stolen, and the mediator or arbitrator will in such cases award the BTC to the sender of the money order, provided they can produce tracking information and receipts. It may be advisable for the sender to write the BTC seller's name on the money order, in order to minimize the risk that the money order is cashed by someone else. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=اطلاعات تماس payment.f2f.contact.prompt=از چه طریقی می‌خواهید توسط همتای معاملاتی با شما تماس حاصل شود؟ (آدرس ایمیل، شماره تلفن، ...) diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 2d126fb1625..6629a86411c 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Médiation support.tab.arbitration.support=Arbitrage support.tab.legacyArbitration.support=Conclusion d'arbitrage support.tab.ArbitratorsSupportTickets=Tickets de {0} -support.filter=Liste de filtre +support.filter=Search disputes support.filter.prompt=Saisissez l'ID du trade, la date, l'adresse "onion" ou les données du compte. +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Envoyer une notification privée +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=Il n'y a pas de tickets ouverts support.sendingMessage=Envoi du message... support.receiverNotOnline=Le destinataire n'est pas en ligne. Le message est enregistré dans leur boîte mail. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=Vous avez demandé une médiation.\n\n{0}\n support.peerOpenedTicket=Votre pair de trading a demandé une assistance en raison de problèmes techniques.\n\n{0}\n\nVersion de Bisq: {1} support.peerOpenedDispute=Votre pair de trading a fait une demande de litige.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Votre pair de trading a demandé une médiation.\n\n{0}\n\nVersion de Bisq: {1} -support.mediatorsDisputeSummary=Message du système:\nSynthèse du litige du médiateur:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Adresse du nœud du médiateur: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Pairs connectés settings.net.onionAddressColumn=Adresse onion settings.net.creationDateColumn=Établi settings.net.connectionTypeColumn=In/Out -settings.net.totalTrafficLabel=Traffic total +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Roundtrip settings.net.sentBytesColumn=Envoyé settings.net.receivedBytesColumn=Reçu @@ -989,7 +995,8 @@ settings.net.heightColumn=Hauteur settings.net.needRestart=Vous devez redémarrer l'application pour appliquer cette modification.\nVous voulez faire cela maintenant? settings.net.notKnownYet=Pas encore connu... -settings.net.sentReceived=Envoyé: {0}, reçu: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[IP address:port | host name:port | onion address:port] (séparés par des virgules). Le port peut être ignoré si utilisé par défaut (8333). settings.net.seedNode=Seed node settings.net.directPeer=Pair (direct) @@ -1011,8 +1018,8 @@ setting.about.support=Soutenir Bisq setting.about.def=Bisq n'est pas une entreprise, c'est un projet ouvert vers la communauté. Si vous souhaitez participer ou soutenir Bisq, veuillez suivre les liens ci-dessous. setting.about.contribute=Contribuer setting.about.providers=Fournisseurs de données -setting.about.apisWithFee=Bisq utilise des APIs tierces ou 3rd party pour le taux de change des devises nationales et des cryptomonnaies, aussi bien que pour obtenir une estimation des frais de minage. -setting.about.apis=Bisq utilise des APIs tierces ou 3rd party pour le taux de change des devises nationales et des cryptomonnaies. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Prix de marché fourni par setting.about.feeEstimation.label=Estimation des frais de minage fournie par setting.about.versionDetails=Détails sur la version @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Ouvrez l''information du p account.tab.arbitratorRegistration=Enregistrement de l'arbitre account.tab.mediatorRegistration=Enregistrement du médiateur account.tab.refundAgentRegistration=Enregistrement de l'agent de remboursement +account.tab.signing=Signing account.tab.account=Compte account.info.headline=Bienvenue sur votre compte Bisq account.info.msg=Ici, vous pouvez ajouter des comptes de trading en devises nationales et en altcoins et créer une sauvegarde de votre portefeuille ainsi que des données de votre compte.\n\nUn nouveau portefeuille Bitcoin a été créé un premier lancement de Bisq.\n\nNous vous recommandons vivement d'écrire les mots-clés de votre seed de portefeuille Bitcoin (voir l'onglet en haut) et d'envisager d'ajouter un mot de passe avant le transfert de fonds. Les dépôts et retraits de Bitcoin sont gérés dans la section \"Fonds\".\n\nNotice de confidentialité et de sécurité : Bisq étant une plateforme d'échange décentralisée, toutes vos données sont conservées sur votre ordinateur. Il n'y a pas de serveurs, nous n'avons donc pas accès à vos informations personnelles, à vos fonds ou même à votre adresse IP. Les données telles que les numéros de compte bancaire, les adresses altcoin & Bitcoin, etc ne sont partagées avec votre pair de trading que pour effectuer les transactions que vous initiez (en cas de litige, le médiateur et l’arbitre verront les mêmes données que votre pair de trading). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Montant du versement de l'acheteur disputeSummaryWindow.payoutAmount.seller=Montant du versement au vendeur disputeSummaryWindow.payoutAmount.invert=Utiliser le perdant comme publicateur disputeSummaryWindow.reason=Motif du litige -disputeSummaryWindow.reason.bug=Bug -disputeSummaryWindow.reason.usability=Utilisabilité -disputeSummaryWindow.reason.protocolViolation=Violation du protocole -disputeSummaryWindow.reason.noReply=Pas de réponse -disputeSummaryWindow.reason.scam=Scam -disputeSummaryWindow.reason.other=Autre -disputeSummaryWindow.reason.bank=Banque + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Utilisabilité +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violation du protocole +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Pas de réponse +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Autre +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banque +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Notes de synthèse disputeSummaryWindow.addSummaryNotes=Ajouter des notes de synthèse disputeSummaryWindow.close.button=Fermer le ticket -disputeSummaryWindow.close.msg=Ticket fermé le {0}\n\nRésumé:\nMontant du paiement pour l''acheteur BTC: {1}\nMontant du paiement pour le vendeur BTC: {2}\n\nNotes de synthèse:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nProchaines étapes:\nOuvrir la transaction et accepter ou rejeter la suggestion du médiateur disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nProchaines étapes:\nAucune autre action n'est requise de votre part. Si l'arbitre a rendu une décision en votre faveur, vous verrez une opération de " Remboursement à la suite de l'arbitrage " dans Fonds/Transactions disputeSummaryWindow.close.closePeer=Vous devez également clore le ticket des pairs de trading ! @@ -1985,7 +2013,8 @@ filterWindow.onions=Adresses onion filtrées (virgule de sep.) filterWindow.accounts=Données filtrées du compte de trading:\nFormat: séparer par une virgule liste des [ID du mode de paiement | champ de données | valeur]. filterWindow.bannedCurrencies=Codes des devises filtrées (séparer avec une virgule.) filterWindow.bannedPaymentMethods=IDs des modes de paiements filtrés (séparer avec une virgule.) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Arbitres filtrés (adresses onion séparées par une virgule) filterWindow.mediators=Médiateurs filtrés (adresses onion sep. par une virgule) filterWindow.refundAgents=Agents de remboursement filtrés (adresses onion sep. par virgule) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=Vous ne disposez pas de suffisamment popup.warning.bsqChangeBelowDustException=Cette transaction crée une BSQ change output qui est inférieure à la dust limit (5,46 BSQ) et serait rejetée par le réseau Bitcoin.\n\nVous devez soit envoyer un montant plus élevé pour éviter la change output (par exemple en ajoutant le montant de dust à votre montant d''envoi), soit ajouter plus de fonds BSQ à votre portefeuille pour éviter de générer une dust output.\n\nLa dust output est {0}. popup.warning.btcChangeBelowDustException=Cette transaction crée une change output qui est inférieure à la dust limit (546 Satoshi) et serait rejetée par le réseau Bitcoin.\n\nVous devez ajouter la quantité de dust à votre montant envoyé pour éviter de générer une dust output.\n\nLa dust output est {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=Vous ne disposez pas de suffisamment de fonds en BSQ pour payer les frais de transaction en BSQ. Vous pouvez payer les frais en BTC ou vous aurez besoin de provisionner votre portefeuille BSQ. Vous pouvez acheter des BSQ sur Bisq.\n\nFonds BSQ manquants: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=Votre portefeuille BSQ ne dispose pas de suffisamment de fonds pour payer les frais de transaction en BSQ. popup.warning.messageTooLong=Votre message dépasse la taille maximale autorisée. Veuillez l'envoyer en plusieurs parties ou le télécharger depuis un service comme https://pastebin.com. popup.warning.lockedUpFunds=Vous avez des fonds bloqués d''une transaction qui a échoué.\nSolde bloqué: {0}\nAdresse de la tx de dépôt: {1}\nID de l''échange: {2}.\n\nVeuillez ouvrir un ticket de support en sélectionnant la transaction dans l'écran des transactions ouvertes et en appuyant sur \"alt + o\" ou \"option + o\". @@ -2437,6 +2466,7 @@ seed.restore.error=Une erreur est survenue lors de la restauration des portefeui payment.account=Compte payment.account.no=N° de compte payment.account.name=Nom du compte +payment.account.userName=User name payment.account.owner=Nom et prénoms du propriétaire du compte payment.account.fullName=Nom complet (prénom, deuxième prénom, nom de famille) payment.account.state=État/Département/Région @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Email ou N° de téléphone payment.venmo.venmoUserName=Nom d'utilisateur Venmo payment.popmoney.accountId=Email ou N° de téléphone -payment.revolut.email=Email -payment.revolut.phoneNr=N° de téléphone enregistré payment.promptPay.promptPayId=N° de carte d'identité/d'identification du contribuable ou numéro de téléphone payment.supportedCurrencies=Devises acceptées payment.limitations=Restrictions @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Veuillez confirmer que votre banque vous permet d'envoyer des dépôts en espèces sur le compte d'autres personnes. Par exemple, Bank of America et Wells Fargo n'autorisent plus de tels dépôts. -payment.revolut.info=Veuillez vous assurer que le numéro de téléphone que vous avez utilisé pour votre compte Revolut est enregistré chez Revolut sinon l'acheteur de BTC ne pourra pas vous envoyer les fonds. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Les mandats sont l'une des méthodes d'achat de fiat les plus privées disponibles sur Bisq.\n\nCependant, veuillez être conscient des risques potentiellement accrus associés à leur utilisation. Bisq n'assumera aucune responsabilité en cas de vol d'un mandat envoyé, et le médiateur ou l'arbitre accordera dans ce cas les BTC à l'expéditeur du mandat, à condition qu'il puisse produire les informations de suivi et les reçus. Il peut être conseillé à l'expéditeur d'écrire le nom du vendeur des BTC sur le mandat, afin de minimiser le risque que le mandat soit encaissé par quelqu'un d'autre. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=information de contact payment.f2f.contact.prompt=Comment souhaitez-vous être contacté par votre pair de trading? (adresse mail, numéro de téléphone,...) diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index b7a9f67485f..0574ac0dad1 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=フィルターリスト +support.filter=Search disputes support.filter.prompt=トレードID、日付、onionアドレスまたはアカウントデータを入力してください +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=プライベート通知を送信 +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=オープンなチケットはありません support.sendingMessage=メッセージを送信中 support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=接続されたピア settings.net.onionAddressColumn=Onionアドレス settings.net.creationDateColumn=既定 settings.net.connectionTypeColumn=イン/アウト -settings.net.totalTrafficLabel=総トラフィック +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=往復 settings.net.sentBytesColumn=送信済 settings.net.receivedBytesColumn=受信済 @@ -989,7 +995,8 @@ settings.net.heightColumn=Height settings.net.needRestart=その変更を適用するには、アプリケーションを再起動する必要があります。\n今すぐ行いますか? settings.net.notKnownYet=まだわかりません... -settings.net.sentReceived=送信済: {0}, 受信済: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[IPアドレス:ポート | ホスト名:ポート | onionアドレス:ポート](コンマ区切り)。デフォルト(8333)が使用される場合、ポートは省略できます。 settings.net.seedNode=シードノード settings.net.directPeer=ピア (ダイレクト) @@ -1011,8 +1018,8 @@ setting.about.support=Bisqをサポートする setting.about.def=Bisqは会社ではなく、開かれたコミュニティのプロジェクトです。Bisqにサポートしたい時は下のURLをチェックしてください。 setting.about.contribute=貢献 setting.about.providers=データプロバイダー -setting.about.apisWithFee=Bisqは、法定通貨とアルトコインの市場価格や、マイニング料金の推定にサードパーティAPIを使用します。 -setting.about.apis=Bisqは法定通貨とアルトコインの市場価格の為にサードパーティAPIを使用します。 +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=市場価格を提供している: setting.about.feeEstimation.label=推定マイニング手数料の提供: setting.about.versionDetails=バージョン詳細 @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar o account.tab.arbitratorRegistration=調停人登録 account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing account.tab.account=アカウント account.info.headline=あなたのBisqアカウントへようこそ! account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=買い手の支払額 disputeSummaryWindow.payoutAmount.seller=売り手の支払額 disputeSummaryWindow.payoutAmount.invert=発行者として敗者を使用 disputeSummaryWindow.reason=係争の理由 -disputeSummaryWindow.reason.bug=バグ -disputeSummaryWindow.reason.usability=使いやすさ -disputeSummaryWindow.reason.protocolViolation=プロトコル違反 -disputeSummaryWindow.reason.noReply=返信無し -disputeSummaryWindow.reason.scam=詐欺 -disputeSummaryWindow.reason.other=その他 -disputeSummaryWindow.reason.bank=銀行 + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=バグ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=使いやすさ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=プロトコル違反 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=返信無し +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=詐欺 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=その他 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=銀行 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=概要ノート disputeSummaryWindow.addSummaryNotes=概要ノートを追加 disputeSummaryWindow.close.button=チケットを閉じる -disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nSummary notes:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=取引相手のチケットも閉じる必要があります! @@ -1985,7 +2013,8 @@ filterWindow.onions=フィルター済onionアドレス(コンマ区切り) filterWindow.accounts=フィルター済トレードアカウントデータ:\n形式: コンマ区切りのリスト [支払方法id | データフィールド | 値] filterWindow.bannedCurrencies=フィルター済通貨コード(コンマ区切り) filterWindow.bannedPaymentMethods=フィルター済支払方法ID(コンマ区切り) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=フィルター済調停人(コンマ区切り onionアドレス) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=あなたはそのトランザクシ popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=BSQのトレード手数料を支払うのに十分なBSQ残高がありません。 BTCで手数料を支払うか、BSQウォレットに入金する必要があります。 BisqでBSQを購入できます。\n\n不足しているBSQ残高:{0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=BSQウォレットにBSQのトレード手数料を支払うのに十分な残高がありません。 popup.warning.messageTooLong=メッセージが許容サイズ上限を超えています。いくつかに分けて送信するか、 https://pastebin.com のようなサービスにアップロードしてください。 popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=シードワードを使用したウォレットの復元中 payment.account=アカウント payment.account.no=アカウント番号 payment.account.name=アカウント名 +payment.account.userName=User name payment.account.owner=アカウント所有者の氏名 payment.account.fullName=氏名(名、ミドルネーム、姓) payment.account.state=州/県/区 @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=メールか電話番号 payment.venmo.venmoUserName=Venmo ユーザー名 payment.popmoney.accountId=メールか電話番号 -payment.revolut.email=メール -payment.revolut.phoneNr=登録された電話番号 payment.promptPay.promptPayId=市民ID/納税者番号または電話番号 payment.supportedCurrencies=サポートされている通貨 payment.limitations=制限事項 @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=あなたの銀行が他の人の口座に現金入金を送ることを許可していることを確認してください。たとえば、Bank of America と Wells Fargo では、こうした預金は許可されなくなりました。 -payment.revolut.info=Revolutアカウントに使用した電話番号がRevolutに登録されていることを確認してください。そうでなければ、BTCの買い手はあなたに資金を送ることができません。 +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Money orders are one of the more private fiat purchase methods available on Bisq.\n\nHowever, please be aware of potentially increased risks associated with their use. Bisq will not bear any responsibility in case a sent money order is stolen, and the mediator or arbitrator will in such cases award the BTC to the sender of the money order, provided they can produce tracking information and receipts. It may be advisable for the sender to write the BTC seller's name on the money order, in order to minimize the risk that the money order is cashed by someone else. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=連絡情報 payment.f2f.contact.prompt=取引相手からどのように連絡を受け取りたいですか?(メールアドレス、電話番号…) diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 186e692a66c..8bc92fc11a8 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediação support.tab.arbitration.support=Arbitragem support.tab.legacyArbitration.support=Arbitração antiga support.tab.ArbitratorsSupportTickets=Tickets de {0} -support.filter=Lista de filtragem +support.filter=Search disputes support.filter.prompt=Insira ID da negociação. data. endereço onion ou dados da conta +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Enviar notificação privada +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=Não há tickets de suporte abertos support.sendingMessage=Enviando mensagem... support.receiverNotOnline=O recipiente não está online. A mensagem será salva na caixa postal dele. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVers support.peerOpenedTicket=O seu parceiro de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão do Bisq: {1} support.peerOpenedDispute=O seu parceiro de negociação solicitou uma disputa.\n\n{0}\n\nVersão do Bisq: {1} support.peerOpenedDisputeForMediation=O seu parceiro de negociação solicitou mediação.\n\n{0}\n\nVersão do Bisq: {1} -support.mediatorsDisputeSummary=Mensagem do sistema:\nResumo da disputa:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Pares conectados settings.net.onionAddressColumn=Endereço onion settings.net.creationDateColumn=Estabelecida settings.net.connectionTypeColumn=Entrada/Saída -settings.net.totalTrafficLabel=Tráfego total +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Ping settings.net.sentBytesColumn=Enviado settings.net.receivedBytesColumn=Recebido @@ -989,7 +995,8 @@ settings.net.heightColumn=Altura settings.net.needRestart=Você precisa reiniciar o programa para aplicar esta alteração.\nDeseja fazer isso agora? settings.net.notKnownYet=Ainda desconhecido... -settings.net.sentReceived=Enviado: {0}, recebido: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[Endeço IP:porta | nome do host:porta | endereço onion:porta] (seperados por vírgulas). A porta pode ser omitida quando a porta padrão (8333) for usada. settings.net.seedNode=Nó semente settings.net.directPeer=Par (direto) @@ -1011,8 +1018,8 @@ setting.about.support=Suporte Bisq setting.about.def=Bisq não é uma empresa — é um projeto aberto à participação da comunidade. Se você tem interesse em participar do projeto ou apoiá-lo, visite os links abaixo. setting.about.contribute=Contribuir setting.about.providers=Provedores de dados -setting.about.apisWithFee=O Bisq utiliza APIs de terceiros para obter os preços de moedas fiduciárias e de altcoins, assim como para estimar a taxa de mineração. -setting.about.apis=Bisq utiliza APIs de terceiros para os preços de moedas fiduciárias e altcoins. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Preços de mercado fornecidos por setting.about.feeEstimation.label=Estimativa da taxa de mineração fornecida por setting.about.versionDetails=Detalhes da versão @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Abrir informações de par account.tab.arbitratorRegistration=Registro de árbitro account.tab.mediatorRegistration=Registro de mediador account.tab.refundAgentRegistration=Registro de agente de reembolsos +account.tab.signing=Signing account.tab.account=Conta account.info.headline=Bem vindo à sua conta Bisq account.info.msg=Aqui você pode adicionar contas de negociação para moedas nacionais & altcoins e criar um backup da sua carteira e dados da sua conta.\n\nUma nova carteira Bitcoin foi criada na primeira vez em que você iniciou a Bisq.\nNós encorajamos fortemente que você anote as palavras semente da sua carteira Bitcoin (veja a aba no topo) e considere adicionar uma senha antes de depositar fundos. Depósitos e retiradas de Bitcoin são gerenciados na seção "Fundos".\n\nNota de privacidade & segurança: visto que a Bisq é uma exchange decentralizada, todos os seus dados são mantidos no seu computador. Não existem servidores, então não temos acesso às suas informações pessoais, seus fundos ou até mesmo ao seu endereço IP. Dados como número de conta bancária, endereços de Bitcoin & altcoin, etc apenas são compartilhados com seu parceiro de negociação para completar as negociações iniciadas por você (em caso de disputa, o mediador ou árbitro verá as mesmas informações que seu parceiro de negociação). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Quantia do pagamento do comprador disputeSummaryWindow.payoutAmount.seller=Quantia de pagamento do vendedor disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador disputeSummaryWindow.reason=Motivo da disputa -disputeSummaryWindow.reason.bug=Bug (problema técnico) -disputeSummaryWindow.reason.usability=Usabilidade -disputeSummaryWindow.reason.protocolViolation=Violação de protocolo -disputeSummaryWindow.reason.noReply=Sem resposta -disputeSummaryWindow.reason.scam=Golpe (Scam) -disputeSummaryWindow.reason.other=Outro -disputeSummaryWindow.reason.bank=Banco + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug (problema técnico) +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilidade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violação de protocolo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Sem resposta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Golpe (Scam) +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Outro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banco +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Notas de resumo disputeSummaryWindow.addSummaryNotes=Adicionar notas de resumo disputeSummaryWindow.close.button=Fechar ticket -disputeSummaryWindow.close.msg=Ticket fechado em {0}\n\nResumo:\nPagamento para comprador de BTC: {1}\nPagamento para vendedor de BTC: {2}\n\nNotas do resumo:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nPróximos passos:\nAbra a negociação e aceite ou recuse a sugestão do mediador disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nPróximos passos:\nNão é preciso fazer mais nada. Se o árbitro decidir em seu favor, você verá uma transação com etiqueta "Reembolso de arbitração" em Fundos/Transações disputeSummaryWindow.close.closePeer=Você também precisa fechar o ticket dos parceiros de negociação! @@ -1985,7 +2013,8 @@ filterWindow.onions=Endereços onion filtrados (sep. por vírgula) filterWindow.accounts=Dados de conta de negociação filtrados:\nFormato: lista separada por vírgulas de [id do método de pagamento | dados | valor] filterWindow.bannedCurrencies=Códigos de moedas filtrados (sep. por vírgula) filterWindow.bannedPaymentMethods=IDs de método de pagamento filtrados (sep. por vírgula) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Árbitros filtrados (endereços onion sep. por vírgula) filterWindow.mediators=Mediadores filtrados (endereços onion separados por vírgula) filterWindow.refundAgents=Agentes de reembolso filtrados (endereços onion separados por vírgula) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=Você não possui fundos BTC suficien popup.warning.bsqChangeBelowDustException=Esta transação cria um troco BSQ menor do que o limite poeira (5.46 BSQ) e seria rejeitada pela rede Bitcoin.\nVocê precisa ou enviar uma quantia maior para evitar o troco (ex: adicionando a quantia poeira ao montante a ser enviado) ou adicionar mais fundos BSQ à sua carteira para evitar gerar uma saída de poeira.\nA saída de poeira é {0}. popup.warning.btcChangeBelowDustException=Esta transação cria um troco menor do que o limite poeira (546 Satoshi) e seria rejeitada pela rede Bitcoin.\nVocê precisa adicionar a quantia poeira ao montante de envio para evitar gerar uma saída de poeira.\nA saída de poeira é {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=Você não tem fundos de BSQ suficientes para pagar a taxa de negociação em BSQ. Você pode pagar a taxa em BTC ou você precisa depositar mais sua carteira BSQ. Você pode comprar BSQ no Bisq.\n\nFundos BSQ faltando: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=Sua carteira BSQ não possui fundos suficientes para pagar a taxa de transação em BSQ. popup.warning.messageTooLong=Sua mensagem excede o tamanho máximo permitido. Favor enviá-la em várias partes ou utilizando um serviço como https://pastebin.com. popup.warning.lockedUpFunds=Você possui fundos travados em uma negociação com erro.\nSaldo travado: {0}\nEndereço da transação de depósito: {1}\nID da negociação: {2}.\n\nPor favor, abra um ticket de suporte selecionando a negociação na tela de negociações em aberto e depois pressionando "\alt+o\" ou \"option+o\". @@ -2437,6 +2466,7 @@ seed.restore.error=Ocorreu um erro ao restaurar as carteiras com palavras sement payment.account=Conta payment.account.no=Nº da conta payment.account.name=Nome da conta +payment.account.userName=User name payment.account.owner=Nome completo do titular da conta payment.account.fullName=Nome completo (nome e sobrenome) payment.account.state=Estado/Província/Região @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag: payment.moneyBeam.accountId=E-mail ou nº de telefone payment.venmo.venmoUserName=Nome do usuário do Venmo payment.popmoney.accountId=E-mail ou nº de telefone -payment.revolut.email=E-mail -payment.revolut.phoneNr=Nº de telefone registrado payment.promptPay.promptPayId=ID de cidadão/ID de impostos ou nº de telefone payment.supportedCurrencies=Moedas disponíveis payment.limitations=Limitações @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Certifique-se de que o seu banco permite a realização de depósitos em espécie na conta de terceiros. -payment.revolut.info=Certifique-se de que o número de telefone que você usou para sua conta Revolut está registrado na Revolut, caso contrário o comprador de BTC não poderá enviar-lhe os fundos. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Ordens de pagamento são uma das formas de compra em dinheiro mais privadas disponíveis na Bisq.\n\nNo entanto, esteja atento ao potencial aumento nos riscos associados com essa forma de operar. A Bisq não se responsabilizará em hipótese alguma caso o dinheiro enviado seja perdido. Além disso, o mediador ou árbitro irá liberar o BTC à parte que envia o dinheiro, desde que existam informações e recibos atrelados ao pagamento. Aconselha-se que quem for enviar o dinheiro escreva o nome do vendedor de BTC na ordem de pagamento minimizando, assim, o risco que esta seja sacada por outra pessoa. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=Informações para contato payment.f2f.contact.prompt=Como prefere ser contatado pelo seu parceiro de negociação? (e-mail, telefone...) diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 70f75827de0..2cc66d379d9 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediação support.tab.arbitration.support=Arbitragem support.tab.legacyArbitration.support=Arbitragem Antiga support.tab.ArbitratorsSupportTickets=Bilhetes de {0} -support.filter=Lista de filtros +support.filter=Search disputes support.filter.prompt=Insira o ID do negócio, data, endereço onion ou dados da conta +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Enviar notificação privada +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=Não há bilhetes abertos support.sendingMessage=Enviando mensagem... support.receiverNotOnline=O recipiente não está online. A mensagem foi guardada na caixa de correio. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=Você solicitou mediação.\n\n{0}\n\nVers support.peerOpenedTicket=O seu par de negociação solicitou suporte devido a problemas técnicos.\n\n{0}\n\nVersão Bisq: {1} support.peerOpenedDispute=O seu par de negociação solicitou uma disputa.\n\n{0}\n\nVersão Bisq: {1} support.peerOpenedDisputeForMediation=O seu par de negociação solicitou uma mediação.\n\n{0}\n\nVersão Bisq: {1} -support.mediatorsDisputeSummary=Messagem do sistema:\nResumo do mediador sobre a disputa:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Endereço do nó do mediador: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Pares conectados settings.net.onionAddressColumn=Endereço onion settings.net.creationDateColumn=Estabelecida settings.net.connectionTypeColumn=Entrando/Saindo -settings.net.totalTrafficLabel=Tráfico total +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Ida-e-volta settings.net.sentBytesColumn=Enviado settings.net.receivedBytesColumn=Recebido @@ -989,7 +995,8 @@ settings.net.heightColumn=Altura settings.net.needRestart=Você precisa reiniciar o programa para aplicar essa alteração.\nVocê quer fazer isso agora?? settings.net.notKnownYet=Ainda desconhecido... -settings.net.sentReceived=Enviado: {0}, recebido: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[endereço IP:porta | nome de host:porta | endereço onion:porta] (separado por vírgula). A porta pode ser omitida se a padrão for usada (8333). settings.net.seedNode=Nó semente settings.net.directPeer=Par (direto) @@ -1011,8 +1018,8 @@ setting.about.support=Apoio Bisq setting.about.def=O Bisq não é uma empresa - é um projeto aberto à comunidade. Se você quiser participar ou apoiar o Bisq, siga os links abaixo. setting.about.contribute=Contribuir setting.about.providers=Provedores de dados -setting.about.apisWithFee=A Bisq usa APIs de terceiros para os preços de mercado de moedas fiduciárias e Altcoin, bem como para estimativas de taxas de mineração. -setting.about.apis=Bisq utiliza APIs de terceiros para os preços de moedas fiduciárias e altcoins. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Preços de mercado fornecidos por setting.about.feeEstimation.label=Taxa de mineração fornecida por setting.about.versionDetails=Detalhes da versão @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Abrir informação do par account.tab.arbitratorRegistration=Registo de árbitro account.tab.mediatorRegistration=Registo do Mediador account.tab.refundAgentRegistration=Registro de agente de reembolso +account.tab.signing=Signing account.tab.account=Conta account.info.headline=Bem vindo à sua conta Bisq account.info.msg=Aqui você pode adicionar contas de negociação para moedas nacionais e altcoins e criar um backup da sua carteira e dos dados da conta.\n\nUma nova carteira de Bitcoin foi criada na primeira vez que você iniciou o Bisq.\n\nÉ altamente recomendável que você anote as sua palavras-semente da carteira do Bitcoin (consulte a guia na parte superior) e considere adicionar uma senha antes do financiamento. Depósitos e retiradas de Bitcoin são gerenciados na secção \"Fundos\".\n\nNota sobre privacidade e segurança: como o Bisq é uma exchange descentralizada, todos os seus dados são mantidos no seu computador. Como não há servidores, não temos acesso às suas informações pessoais, fundos ou mesmo seu endereço IP. Dados como números de contas bancárias, endereços de altcoin e Bitcoin etc. são compartilhados apenas com seu par de negociação para realizar negociações iniciadas (no caso de uma disputa, o mediador ou o árbitro verá os mesmos dados que o seu parceiro de negociação). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Quantia de pagamento do comprador disputeSummaryWindow.payoutAmount.seller=Quantia de pagamento do vendedor disputeSummaryWindow.payoutAmount.invert=Usar perdedor como publicador disputeSummaryWindow.reason=Razão da disputa -disputeSummaryWindow.reason.bug=Erro -disputeSummaryWindow.reason.usability=Usabilidade -disputeSummaryWindow.reason.protocolViolation=Violação de protocolo -disputeSummaryWindow.reason.noReply=Sem resposta -disputeSummaryWindow.reason.scam=Golpe -disputeSummaryWindow.reason.other=Outro -disputeSummaryWindow.reason.bank=Banco + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Erro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Usabilidade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Violação de protocolo +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Sem resposta +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Golpe +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Outro +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Banco +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Notas de resumo disputeSummaryWindow.addSummaryNotes=Adicionar notas de resumo disputeSummaryWindow.close.button=Fechar bilhete -disputeSummaryWindow.close.msg=Bilhete fechado em {0}\n\nResumo:\nQuantia do pagamento para comprador de BTC: {1}\nQuantia do pagamento para vendedor de BTC: {2}\n\nNotas de resumo:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nPróximos passos:\nAbrir negócio e aceitar ou rejeitar as sugestões do mediador disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nPróximos passos:\nNenhuma ação adicional é necessária. Se o árbitro decidir a seu favor, você verá uma transação de "Reembolso da arbitragem" em Fundos/Transações disputeSummaryWindow.close.closePeer=Você também precisa fechar o bilhete dos pares de negociação! @@ -1985,7 +2013,8 @@ filterWindow.onions=Endereços onion filtrados (sep. por vírgula) filterWindow.accounts=Dados da conta de negociação filtrados:\nFormato: lista de [id de método de pagamento | campo de dados | valor] sep. por vírgula filterWindow.bannedCurrencies=Códigos de moedas filtrados (sep. por vírgula) filterWindow.bannedPaymentMethods=IDs de método de pagamento filtrados (sep. por vírgula) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Árbitros filtrados (endereços onion sep. por vírgula) filterWindow.mediators=Mediadores filtrados (endereços onion separados por vírgula) filterWindow.refundAgents=Agentes de reembolso filtrados (endereços onion sep. por virgula) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=Você não tem fundos BTC suficientes popup.warning.bsqChangeBelowDustException=Esta transação cria um output de trocos de BSQ que está abaixo do limite de poeira (5,46 BSQ) e seria rejeitada pela rede do bitcoin.\n\nVocê precisa enviar uma quantia mais elevada para evitar o output de trocos (por exemplo, adicionando a quantia de poeira ao seu montante a ser enviado) ou adicionar mais fundos de BSQ à sua carteira de modo a evitar a gerar um output de poeira.\n\nO output de poeira é {0}. popup.warning.btcChangeBelowDustException=Esta transação cria um output de trocos que está abaixo do limite de poeira (546 satoshis) e seria rejeitada pela rede do bitcoin.\n\nVocê precisa adicionar a quantia de poeira ao seu montante à ser enviado para evitar gerar um output de poeira.\n\nO output de poeira é {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=Você não tem fundos de BSQ suficientes para pagar a taxa de negócio em BSQ. Você pode pagar a taxa em BTC ou você precisa financiar sua carteira BSQ. Você pode comprar BSQ no Bisq.\n\nFundos BSQ em falta: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=Sua carteira BSQ não possui fundos suficientes para pagar a taxa de negócio em BSQ. popup.warning.messageTooLong=Sua mensagem excede o tamanho máx. permitido. Por favor enviá-la em várias partes ou carregá-la utilizando um serviço como https://pastebin.com. popup.warning.lockedUpFunds=Você trancou fundos de um negócio falhado..\nSaldo trancado: {0} \nEndereço da tx de Depósito: {1}\nID de negócio: {2}.\n\nPor favor abra um bilhete de apoio selecionando o negócio no ecrã de negócios abertos e pressione \"alt + o\" ou \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=Um erro ocorreu ao restaurar as carteiras com palavras-sement payment.account=Conta payment.account.no=Nº da conta payment.account.name=Nome da conta +payment.account.userName=User name payment.account.owner=Nome completo do titular da conta payment.account.fullName=Nome completo (primeiro, nome do meio, último) payment.account.state=Estado/Província/Região @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Email ou nº de telemóvel payment.venmo.venmoUserName=Nome de utilizador do Venmo payment.popmoney.accountId=Email ou nº de telemóvel -payment.revolut.email=Email -payment.revolut.phoneNr=Nº de telemóvel registado payment.promptPay.promptPayId=ID de cidadão/ID de impostos ou nº de telemóvel payment.supportedCurrencies=Moedas suportadas payment.limitations=Limitações @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Por favor, confirme que seu banco permite-lhe enviar depósitos em dinheiro para contas de outras pessoas. Por exemplo, o Bank of America e o Wells Fargo não permitem mais esses depósitos. -payment.revolut.info=Certifique-se de que o número de telemóvel que você usou para sua conta Revolut está registado na Revolut, caso contrário o comprador de BTC não poderá enviar-lhe os fundos. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=As Ordens de pagamento são um dos métodos de compra de moedas fiduciárias mais privados disponíveis no Bisq.\n\nEntretanto, esteja ciente dos riscos potencialmente aumentados associados ao seu uso. O Bisq não assumirá qualquer responsabilidade no caso de uma ordem de pagamento enviada ser roubada, e o mediador ou o árbitro, nesses casos, concederão o BTC ao remetente da ordem de pagamento, desde que possam produzir informações de rastreamento e recibos. Pode ser aconselhável que o remetente escreva o nome do vendedor de BTC na ordem de pagamento, a fim de minimizar o risco de que a ordem de pagamento seja levantado por outra pessoa. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=Informação de contacto payment.f2f.contact.prompt=Como deseja ser contactado pelo par de negociação? (endereço de e-mail, número de telefone, ...) diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 74cd61e260e..c0124177f9a 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Фильтры +support.filter=Search disputes support.filter.prompt=Введите идентификатор сделки, дату, onion-адрес или данные учётной записи +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Отправить личное уведомление +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=Нет текущих обращений support.sendingMessage=Отправка сообщения... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Подключенные пиры settings.net.onionAddressColumn=Onion-адрес settings.net.creationDateColumn=Создано settings.net.connectionTypeColumn=Вх./Вых. -settings.net.totalTrafficLabel=Общий трафик +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Задержка settings.net.sentBytesColumn=Отправлено settings.net.receivedBytesColumn=Получено @@ -989,7 +995,8 @@ settings.net.heightColumn=Height settings.net.needRestart=Необходимо перезагрузить приложение, чтобы применить это изменение.\nСделать это сейчас? settings.net.notKnownYet=Пока неизвестно... -settings.net.sentReceived=Отправлено: {0}, получено: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[IP-адрес:порт | хост:порт | onion-адрес:порт] (через запятые). Порт можно не указывать, если используется порт по умолчанию (8333). settings.net.seedNode=Исходный узел settings.net.directPeer=Пир (прямой) @@ -1011,8 +1018,8 @@ setting.about.support=Поддержать Bisq setting.about.def=Bisq не является компанией, а представляет собой общественный проект, открытый для участия. Если вы хотите принять участие или поддержать Bisq, перейдите по ссылкам ниже. setting.about.contribute=Помочь setting.about.providers=Источники данных -setting.about.apisWithFee=Bisq использует сторонние API для определения рыночного курса валют и альткойнов, а также расчёта комиссии майнера. -setting.about.apis=Bisq использует сторонние API для определения рыночного курса валют и альткойнов. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Рыночный курс предоставлен setting.about.feeEstimation.label=Расчёт комиссии майнера предоставлен setting.about.versionDetails=Подробности версии @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar o account.tab.arbitratorRegistration=Регистрация арбитра account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing account.tab.account=Счёт account.info.headline=Добро пожаловать в ваш счёт Bisq account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Сумма выплаты покупат disputeSummaryWindow.payoutAmount.seller=Сумма выплаты продавца disputeSummaryWindow.payoutAmount.invert=Проигравший публикует disputeSummaryWindow.reason=Причина спора -disputeSummaryWindow.reason.bug=Ошибка -disputeSummaryWindow.reason.usability=Удобство использования -disputeSummaryWindow.reason.protocolViolation=Нарушение протокола -disputeSummaryWindow.reason.noReply=Отсутствие ответа -disputeSummaryWindow.reason.scam=Мошенничество -disputeSummaryWindow.reason.other=Другое -disputeSummaryWindow.reason.bank=Банк + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Ошибка +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Удобство использования +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Нарушение протокола +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Отсутствие ответа +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Мошенничество +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Другое +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Банк +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Примечания disputeSummaryWindow.addSummaryNotes=Добавить примечания disputeSummaryWindow.close.button=Закрыть обращение -disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nSummary notes:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Вам также необходимо закрыть обращение контрагента! @@ -1985,7 +2013,8 @@ filterWindow.onions=Отфильтрованные onion-адреса (чере filterWindow.accounts=Отфильтрованные данные торгового счёта:\nФормат: список, через запятые [идентификатор метода платежа | поле данных | значение] filterWindow.bannedCurrencies=Отфильтрованные коды валют (через запят.) filterWindow.bannedPaymentMethods=Отфильтрованные идент. методов платежа (через запят.) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Отфильтрованные арбитры (onion-адреса через запят.) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=У вас недостаточно BT popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=У Вас недостаточно BSQ для оплаты комиссии в BSQ. Вы можете оплатить комиссию в BTC или пополнить свой кошелёк BSQ. BSQ можно купить в Bisq.\n\nНедостающая сумма в BSQ: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=В вашем кошельке BSQ недостаточно средств для оплаты комиссии за сделку в BSQ. popup.warning.messageTooLong=Ваше сообщение превышает макс. разрешённый размер. Разбейте его на несколько частей или загрузите в веб-приложение для работы с отрывками текста, например https://pastebin.com. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=Произошла ошибка при восстановле payment.account=Счёт payment.account.no=Номер счёта payment.account.name=Название счёта +payment.account.userName=User name payment.account.owner=Полное имя владельца счёта payment.account.fullName=Полное имя (имя, отчество, фамилия) payment.account.state=Штат/Провинция/Область @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Эл. адрес или тел. номер payment.venmo.venmoUserName=Имя пользователя Venmo payment.popmoney.accountId=Эл. адрес или тел. номер -payment.revolut.email=Электронный адрес -payment.revolut.phoneNr=Зарегистрированный номер телефона payment.promptPay.promptPayId=Удостовер. личности / налог. номер или номер телефона payment.supportedCurrencies=Поддерживаемые валюты payment.limitations=Ограничения @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Убедитесь, что ваш банк позволяет отправлять денежные переводы на счета других лиц. Например, Bank of America и Wells Fargo больше не разрешают такие переводы. -payment.revolut.info=Убедитесь, что номер телефона, который вы использовали для своего счета Revolut, зарегистрирован в Revolut. Иначе, покупатель BTC не сможет отправить вам средства. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Money orders are one of the more private fiat purchase methods available on Bisq.\n\nHowever, please be aware of potentially increased risks associated with their use. Bisq will not bear any responsibility in case a sent money order is stolen, and the mediator or arbitrator will in such cases award the BTC to the sender of the money order, provided they can produce tracking information and receipts. It may be advisable for the sender to write the BTC seller's name on the money order, in order to minimize the risk that the money order is cashed by someone else. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=Контактная информация payment.f2f.contact.prompt=Как вы хотите связаться с контрагентом? (электронная почта, телефон...) diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 7688013f4a0..378c2344b48 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=รายการตัวกรอง +support.filter=Search disputes support.filter.prompt=Enter trade ID, date, onion address or account data +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=ส่งการแจ้งเตือนส่วนตัว +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=ไม่มีการเปิดรับคำขอร้องหรือความช่วยเหลือ support.sendingMessage=กำลังส่งข้อความ... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=เชื่อมต่อกับเน็ตเ settings.net.onionAddressColumn=ที่อยู่ Onion settings.net.creationDateColumn=ที่จัดตั้งขึ้น settings.net.connectionTypeColumn=เข้า/ออก -settings.net.totalTrafficLabel=ปริมาณการเข้าชมทั้งหมด +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=ไป - กลับ settings.net.sentBytesColumn=ส่งแล้ว settings.net.receivedBytesColumn=ได้รับแล้ว @@ -989,7 +995,8 @@ settings.net.heightColumn=Height settings.net.needRestart=คุณต้องรีสตาร์ทแอ็พพลิเคชั่นเพื่อทำให้การเปลี่ยนแปลงนั้นเป็นผล\nคุณต้องการทำตอนนี้หรือไม่ settings.net.notKnownYet=ยังไม่ทราบ ... -settings.net.sentReceived=ส่ง: {0} ได้รับ: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[ที่อยู่ IP: พอร์ต | ชื่อโฮสต์: พอร์ต | ที่อยู่ onion: พอร์ต] (คั่นด้วยเครื่องหมายจุลภาค) Port สามารถละเว้นได้ถ้าใช้ค่าเริ่มต้น (8333) settings.net.seedNode=แหล่งโหนดข้อมูล settings.net.directPeer=Peer (โดยตรง) @@ -1011,8 +1018,8 @@ setting.about.support=สนับสนุน Bisq setting.about.def=Bisq ไม่ใช่ บริษัท แต่เป็นโปรเจคชุมชนและเปิดให้คนมีส่วนร่วม ถ้าคุณต้องการจะเข้าร่วมหรือสนับสนุน Bisq โปรดทำตามลิงค์ข้างล่างนี้ setting.about.contribute=สนับสนุน setting.about.providers=ผู้ให้บริการข้อมูล -setting.about.apisWithFee=Bisq ใช้ APIs ของบุคคลที่ 3 สำหรับราคาตลาดของ Fiat และ Altcoin ตลอดจนการประมาณค่าการขุด -setting.about.apis=Bisq ใช้ APIs ของบุคคลที่ 3 สำหรับ Fiat และ Altcoin ในราคาตลาด +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=ราคาตลาดจัดโดย setting.about.feeEstimation.label=การประมาณค่าธรรมเนียมการขุดโดย setting.about.versionDetails=รายละเอียดของเวอร์ชั่น @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar o account.tab.arbitratorRegistration=การลงทะเบียนผู้ไกล่เกลี่ย account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing account.tab.account=บัญชี account.info.headline=ยินดีต้อนรับสู่บัญชี Bisq ของคุณ account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=จำนวนเงินที่จ disputeSummaryWindow.payoutAmount.seller=จำนวนเงินที่จ่ายของผู้ขาย disputeSummaryWindow.payoutAmount.invert=ใช้ผู้แพ้เป็นผู้เผยแพร่ disputeSummaryWindow.reason=เหตุผลในการพิพาท -disputeSummaryWindow.reason.bug=ปัญหา -disputeSummaryWindow.reason.usability=การใช้งาน -disputeSummaryWindow.reason.protocolViolation=การละเมิดโปรโตคอล -disputeSummaryWindow.reason.noReply=ไม่มีการตอบ -disputeSummaryWindow.reason.scam=การหลอกลวง -disputeSummaryWindow.reason.other=อื่น ๆ -disputeSummaryWindow.reason.bank=ธนาคาร + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=ปัญหา +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=การใช้งาน +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=การละเมิดโปรโตคอล +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=ไม่มีการตอบ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=การหลอกลวง +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=อื่น ๆ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=ธนาคาร +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=สรุปบันทึกย่อ disputeSummaryWindow.addSummaryNotes=เพิ่มสรุปบันทึกย่อ: disputeSummaryWindow.close.button=ปิดการยื่นคำขอและความช่วยเหลือ -disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nSummary notes:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=คุณจำเป็นต้องยุติคำขอความช่วยเหลือคู่ค้าด้วย ! @@ -1985,7 +2013,8 @@ filterWindow.onions=ที่อยู่ onion ที่ได้รับก filterWindow.accounts=ข้อมูลบัญชีการซื้อขายที่ถูกกรอง: \nรูปแบบ: เครื่องหมายจุลภาค รายการของ [id วิธีการชำระเงิน | ด้านข้อมูล | มูลค่า] filterWindow.bannedCurrencies=รหัสโค้ดสกุลเงินที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) filterWindow.bannedPaymentMethods=รหัส ID วิธีการชำระเงินที่ได้รับการกรอง (คั่นด้วยเครื่องหมายจุลภาค) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=ผู้ไกล่เกลี่ยที่ได้รับการคัดกรอง (คั่นด้วยเครื่องหมายจุลภาค ที่อยู่ onion) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=คุณไม่มีเงิน popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=คุณไม่มีเงินทุน BSQ เพียงพอสำหรับการจ่ายค่าธรรมเนียมการเทรดใน BSQ คุณสามารถชำระได้ใน BTC หรือคุณสามารถใช้เงินทุนจากกระเป๋าสตางค์ BSQ ของคุณ คุณสามารถซื้อ BSQ ใน Bisq ได้ \n\nกองทุน BSQ ที่หายไป: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=กระเป๋าสตางค์ BSQ ของคุณไม่มีจำนวนเงินทุนที่มากพอสำหรับการชำระการเทรดใน BSQ popup.warning.messageTooLong=ข้อความของคุณเกินขีดจำกัดสูงสุดที่อนุญาต โปรดแบ่งส่งเป็นหลายส่วนหรืออัปโหลดไปยังบริการเช่น https://pastebin.com popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=เกิดข้อผิดพลาดขณะกู้ payment.account=บัญชี payment.account.no=หมายเลขบัญชี payment.account.name=ชื่อบัญชี +payment.account.userName=User name payment.account.owner=ชื่อเต็มของเจ้าของบัญชี payment.account.fullName=ชื่อเต็ม (ชื่อจริง, ชื่อกลาง, นามสกุล) payment.account.state=รัฐ / จังหวัด / ภูมิภาค @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=อีเมลหรือหมายเลขโทรศัพท์ payment.venmo.venmoUserName=ชื่อผู้ใช้ Venmo payment.popmoney.accountId=อีเมลหรือหมายเลขโทรศัพท์ -payment.revolut.email=อีเมล -payment.revolut.phoneNr=Registered phone no. payment.promptPay.promptPayId=รหัสบัตรประชาชน/รหัสประจำตัวผู้เสียภาษี หรือเบอร์โทรศัพท์ payment.supportedCurrencies=สกุลเงินที่ได้รับการสนับสนุน payment.limitations=ข้อจำกัด @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=โปรดยืนยันว่าธนาคารของคุณได้อนุมัติให้คุณสามารถส่งเงินสดให้กับบัญชีบุคคลอื่นได้ ตัวอย่างเช่น บางธนาคารที่ไม่ได้มีการบริการถ่ายโอนเงินสดอย่าง Bank of America และ Wells Fargo -payment.revolut.info=Please be sure that the phone number you used for your Revolut account is registered at Revolut otherwise the BTC buyer cannot send you the funds. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Money orders are one of the more private fiat purchase methods available on Bisq.\n\nHowever, please be aware of potentially increased risks associated with their use. Bisq will not bear any responsibility in case a sent money order is stolen, and the mediator or arbitrator will in such cases award the BTC to the sender of the money order, provided they can produce tracking information and receipts. It may be advisable for the sender to write the BTC seller's name on the money order, in order to minimize the risk that the money order is cashed by someone else. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=ข้อมูลติดต่อ payment.f2f.contact.prompt=วิธีการที่คุณต้องการได้รับการติดต่อจากการค้าจากระบบ peer (ที่อยู่อีเมล , หมายเลขโทรศัพท์ ... ) diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 137dd2909f9..5bf014ceaa4 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets -support.filter=Danh sách lọc +support.filter=Search disputes support.filter.prompt=Nhập ID giao dịch, ngày tháng, địa chỉ onion hoặc dữ liệu tài khoản +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=Gửi thông báo riêng tư +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=Không có đơn hỗ trợ được mở support.sendingMessage=Đang gửi tin nhắn... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=You requested mediation.\n\n{0}\n\nBisq ver support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0}\n\nBisq version: {1} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} -support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=Các đối tác được kết nối settings.net.onionAddressColumn=Địa chỉ onion settings.net.creationDateColumn=Đã thiết lập settings.net.connectionTypeColumn=Vào/Ra -settings.net.totalTrafficLabel=Tổng lưu lượng +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=Khứ hồi settings.net.sentBytesColumn=Đã gửi settings.net.receivedBytesColumn=Đã nhận @@ -989,7 +995,8 @@ settings.net.heightColumn=Height settings.net.needRestart=Bạn cần khởi động lại ứng dụng để thay đổi.\nBạn có muốn khởi động bây giờ không? settings.net.notKnownYet=Chưa biết... -settings.net.sentReceived=Đã gửi: {0}, đã nhận: {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=[Địa chỉ IP:tên cổng | máy chủ:cổng | Địa chỉ onion:cổng] (tách bằng dấu phẩy). Cổng có thể bỏ qua nếu sử dụng mặc định (8333). settings.net.seedNode=nút cung cấp thông tin settings.net.directPeer=Đối tác (trực tiếp) @@ -1011,8 +1018,8 @@ setting.about.support=Hỗ trợ Bisq setting.about.def=Bisq không phải là một công ty mà là một dự án mở cho cả cộng đồng. Nếu bạn muốn tham gia hoặc hỗ trợ Bisq, vui lòng truy cập link dưới đây. setting.about.contribute=Góp vốn setting.about.providers=Nhà cung cấp dữ liệu -setting.about.apisWithFee=Bisq sử dụng API bên thứ 3 để ước tính giá thị trường Fiat và Altcoin cũng như phí đào. -setting.about.apis=Bisq sử dụng API bên thứ 3 để ước tính giá thị trường Fiat và Altcoin. +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=Giá thị trường cung cấp bởi setting.about.feeEstimation.label=Ước tính phí đào cung cấp bởi setting.about.versionDetails=Thông tin về phiên bản @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=Open peer info at avatar o account.tab.arbitratorRegistration=Đăng ký trọng tài account.tab.mediatorRegistration=Mediator registration account.tab.refundAgentRegistration=Refund agent registration +account.tab.signing=Signing account.tab.account=Tài khoản account.info.headline=Chào mừng đến với tài khoản Bisq của bạn account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\nA new Bitcoin wallet was created the first time you started Bisq.\n\nWe strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\nPrivacy & security note: because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=Khoản tiền hoàn lại của ngườ disputeSummaryWindow.payoutAmount.seller=Khoản tiền hoàn lại của người bán disputeSummaryWindow.payoutAmount.invert=Sử dụng người thua như người công bố disputeSummaryWindow.reason=Lý do khiếu nại -disputeSummaryWindow.reason.bug=Sự cố -disputeSummaryWindow.reason.usability=Khả năng sử dụng -disputeSummaryWindow.reason.protocolViolation=Vi phạm giao thức -disputeSummaryWindow.reason.noReply=Không có phản hồi -disputeSummaryWindow.reason.scam=Scam -disputeSummaryWindow.reason.other=Khác -disputeSummaryWindow.reason.bank=Ngân hàng + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Sự cố +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=Khả năng sử dụng +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=Vi phạm giao thức +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=Không có phản hồi +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=Scam +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=Khác +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=Ngân hàng +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=Lưu ý tóm tắt disputeSummaryWindow.addSummaryNotes=Thêm lưu ý tóm tắt disputeSummaryWindow.close.button=Đóng đơn -disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nSummary notes:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\nOpen trade and accept or reject suggestion from mediator disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\nNo further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=Bạn cũng cần phải đóng Đơn Đối tác giao dịch! @@ -1985,7 +2013,8 @@ filterWindow.onions=Địa chỉ onion đã lọc (cách nhau bằng dấu phẩ filterWindow.accounts=Dữ liệu tài khoản giao dịch đã lọc:\nĐịnh dạng: cách nhau bằng dấu phẩy danh sách [ID phương thức thanh toán | trường dữ liệu | giá trị] filterWindow.bannedCurrencies=Mã tiền tệ đã lọc (cách nhau bằng dấu phẩy) filterWindow.bannedPaymentMethods=ID phương thức thanh toán đã lọc (cách nhau bằng dấu phẩy) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=Các trọng tài đã lọc (địa chỉ onion cách nhau bằng dấu phẩy) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=Bạn không có đủ vốn BTC đ popup.warning.bsqChangeBelowDustException=This transaction creates a BSQ change output which is below dust limit (5.46 BSQ) and would be rejected by the Bitcoin network.\n\nYou need to either send a higher amount to avoid the change output (e.g. by adding the dust amount to your sending amount) or add more BSQ funds to your wallet so you avoid to generate a dust output.\n\nThe dust output is {0}. popup.warning.btcChangeBelowDustException=This transaction creates a change output which is below dust limit (546 Satoshi) and would be rejected by the Bitcoin network.\n\nYou need to add the dust amount to your sending amount to avoid to generate a dust output.\n\nThe dust output is {0}. -popup.warning.insufficientBsqFundsForBtcFeePayment=Bạn không đủ BSQ để thanh toán phí giao dịch bằng BSQ. Bạn có thể thanh toán phí bằng BTC hoặc nạp tiền vào ví BSQ của bạn. Bạn có thể mua BSQ tại Bisq.\nSố BSQ còn thiếu: {0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=Ví BSQ của bạn không đủ tiền để trả phí giao dịch bằng BSQ. popup.warning.messageTooLong=Tin nhắn của bạn vượt quá kích cỡ tối đa cho phép. Vui lòng gửi thành nhiều lần hoặc tải lên mạng như https://pastebin.com. popup.warning.lockedUpFunds=You have locked up funds from a failed trade.\nLocked up balance: {0} \nDeposit tx address: {1}\nTrade ID: {2}.\n\nPlease open a support ticket by selecting the trade in the open trades screen and pressing \"alt + o\" or \"option + o\"." @@ -2437,6 +2466,7 @@ seed.restore.error=Có lỗi xảy ra khi khôi phục ví với Seed words.{0} payment.account=Tài khoản payment.account.no=Tài khoản số payment.account.name=Tên tài khoản +payment.account.userName=User name payment.account.owner=Họ tên chủ tài khoản payment.account.fullName=Họ tên (họ, tên lót, tên) payment.account.state=Bang/Tỉnh/Vùng @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=Email hoặc số điện thoại payment.venmo.venmoUserName=Tên người dùng Venmo payment.popmoney.accountId=Email hoặc số điện thoại -payment.revolut.email=Email -payment.revolut.phoneNr=Số điện thoại đã đăng ký. payment.promptPay.promptPayId=ID công dân/ ID thuế hoặc số điện thoại payment.supportedCurrencies=Tiền tệ hỗ trợ payment.limitations=Hạn chế @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=Vui lòng xác nhận rằng ngân hàng của bạn cho phép nạp tiền mặt vào tài khoản của người khác. Chẳng hạn, Ngân Hàng Mỹ và Wells Fargo không còn cho phép nạp tiền như vậy nữa. -payment.revolut.info=Vui lòng đảm bảo rằng số điện thoại bạn dùng cho tài khoản Revolut đã được đăng ký tại Revolut nếu không thì người mua BTC sẽ không thể chuyển tiền cho bạn. +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=Money orders are one of the more private fiat purchase methods available on Bisq.\n\nHowever, please be aware of potentially increased risks associated with their use. Bisq will not bear any responsibility in case a sent money order is stolen, and the mediator or arbitrator will in such cases award the BTC to the sender of the money order, provided they can produce tracking information and receipts. It may be advisable for the sender to write the BTC seller's name on the money order, in order to minimize the risk that the money order is cashed by someone else. +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=thông tin liên hệ payment.f2f.contact.prompt=Bạn muốn liên hệ với đối tác giao dịch qua đâu? (email, địa chỉ, số điện thoại,....) diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 24259e586ca..7c08c8015b7 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=调解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=历史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工单 -support.filter=筛选列表 +support.filter=Search disputes support.filter.prompt=输入 交易 ID、日期、洋葱地址或账户信息 +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=发送私人通知 +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=没有创建的话题 support.sendingMessage=发送消息... support.receiverNotOnline=收件人未在线。消息被保存到他们的邮箱。 @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=您创建了一个调解请求。\n\n{0}\n\ support.peerOpenedTicket=对方因技术问题请求获取帮助。\n\n{0}\n\nBisq 版本:{1} support.peerOpenedDispute=对方创建了一个纠纷请求。\n\n{0}\n\nBisq 版本:{1} support.peerOpenedDisputeForMediation=对方创建了一个调解请求。\n\n{0}\n\nBisq 版本:{1} -support.mediatorsDisputeSummary=系统消息:\n调解纠纷总结:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=仲裁员的节点地址:{0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=已连接节点 settings.net.onionAddressColumn=匿名地址 settings.net.creationDateColumn=已建立连接 settings.net.connectionTypeColumn=入/出 -settings.net.totalTrafficLabel=总流量: +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=延迟 settings.net.sentBytesColumn=发送 settings.net.receivedBytesColumn=接收 @@ -989,7 +995,8 @@ settings.net.heightColumn=高度 settings.net.needRestart=您需要重启应用程序以同意这次变更。\n您需要现在重启吗? settings.net.notKnownYet=至今未知... -settings.net.sentReceived=发送 {0},接收 {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=添加逗号分隔的 IP 地址及端口,如使用8333端口可不填写。 settings.net.seedNode=种子节点 settings.net.directPeer=节点(直连) @@ -1011,8 +1018,8 @@ setting.about.support=支持 Bisq setting.about.def=Bisq 不是一个公司,而是一个社区项目,开放参与。如果您想参与或支持 Bisq,请点击下面连接。 setting.about.contribute=贡献 setting.about.providers=数据提供商 -setting.about.apisWithFee=Bisq 使用第三方 API 获取法定货币与虚拟币的市场价以及矿工手续费的估价。 -setting.about.apis=Bisq 使用第三方 API 获取法定货币与虚拟币的市场价。 +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=交易所价格提供商 setting.about.feeEstimation.label=矿工手续费估算提供商 setting.about.versionDetails=版本详情 @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=打开在头像或纠纷 account.tab.arbitratorRegistration=仲裁员注册 account.tab.mediatorRegistration=调解员注册 account.tab.refundAgentRegistration=退款助理注册 +account.tab.signing=Signing account.tab.account=账户 account.info.headline=欢迎来到 Bisq 账户 account.info.msg=在这里你可以设置交易账户的法定货币及数字货币,选择仲裁员和备份你的钱包及账户数据。\n\n当你开始运行 Bisq 就已经创建了一个空的比特币钱包。\n\n我们建议你在充值之前写下你比特币钱包的还原密钥(在左边的列表)和考虑添加密码。在“资金”选项中管理比特币存入和提现。\n\n隐私 & 安全:\nBisq 是一个去中心化的交易所 – 意味着您的所有数据都保存在您的电脑上,没有服务器,我们无法访问您的个人信息,您的资金,甚至您的 IP 地址。如银行账号、数字货币、比特币地址等数据只分享给与您交易的人,以实现您发起的交易(如果有争议,仲裁员将会看到您的交易数据)。 @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=买家支付金额 disputeSummaryWindow.payoutAmount.seller=卖家支付金额 disputeSummaryWindow.payoutAmount.invert=使用失败者作为发布者 disputeSummaryWindow.reason=纠纷的原因 -disputeSummaryWindow.reason.bug=Bug -disputeSummaryWindow.reason.usability=可用性 -disputeSummaryWindow.reason.protocolViolation=违反协议 -disputeSummaryWindow.reason.noReply=不回复 -disputeSummaryWindow.reason.scam=诈骗 -disputeSummaryWindow.reason.other=其他 -disputeSummaryWindow.reason.bank=银行 + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=可用性 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=违反协议 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=不回复 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=诈骗 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=其他 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=银行 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=总结说明 disputeSummaryWindow.addSummaryNotes=添加总结说明 disputeSummaryWindow.close.button=关闭话题 -disputeSummaryWindow.close.msg=工单已关闭 {0}\n\n摘要:\nBTC 买家的支付金额:{1}\nBTC 卖家的支付金额:{2}\n\n总结说明:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\n下一个步骤:\n打开未完成交易,接受或拒绝建议的调解员的建议 disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\n下一个步骤:\n不需要您采取进一步的行动。如果仲裁员做出了对你有利的裁决,你将在 资金/交易 页中看到“仲裁退款”交易 disputeSummaryWindow.close.closePeer=你也需要关闭交易对象的话题! @@ -1985,7 +2013,8 @@ filterWindow.onions=筛选匿名地址(用逗号“,”隔开) filterWindow.accounts=筛选交易账户数据:\n格式:逗号分割的 [付款方式ID|数据字段|值] filterWindow.bannedCurrencies=筛选货币代码(用逗号“,”隔开) filterWindow.bannedPaymentMethods=筛选支付方式 ID(用逗号“,”隔开) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=筛选后的仲裁人(用逗号“,”隔开的洋葱地址) filterWindow.mediators=筛选后的调解员(用逗号“,”隔开的洋葱地址) filterWindow.refundAgents=筛选后的退款助理(用逗号“,”隔开的洋葱地址) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=你没有足够的 BTC 资金支付 popup.warning.bsqChangeBelowDustException=该交易产生的 BSQ 变化输出低于零头限制(5.46 BSQ),将被比特币网络拒绝。\n\n您需要发送更高的金额以避免更改输出(例如,通过在您的发送金额中添加零头),或者向您的钱包中添加更多的 BSQ 资金,以避免生成零头输出。\n\n零头输出为 {0}。 popup.warning.btcChangeBelowDustException=该交易创建的更改输出低于零头限制(546 聪),将被比特币网络拒绝。\n\n您需要将零头添加到发送量中,以避免生成零头输出。\n\n零头输出为{0}。 -popup.warning.insufficientBsqFundsForBtcFeePayment=您没有足够的 BSQ 资金支付 BSQ 的交易费用。您可以在 BTC 支付费用,或者您需要充值您的 BSQ 钱包。你可以在 Bisq 买到 BSQ 。\n\n缺少 BSQ 资金:{0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=您的 BSQ 钱包没有足够的资金支付 BSQ 的交易费用。 popup.warning.messageTooLong=您的信息超过最大允许的大小。请将其分成多个部分发送,或将其上传到 https://pastebin.com 之类的服务器。 popup.warning.lockedUpFunds=你已经从一个失败的交易中冻结了资金。\n冻结余额:{0}\n存款tx地址:{1}\n交易单号:{2}\n\n请通过选择待处理交易界面中的交易并点击“alt + o”或“option+ o”打开帮助话题。 @@ -2437,6 +2466,7 @@ seed.restore.error=使用还原密钥恢复钱包时出现错误。{0} payment.account=账户 payment.account.no=账户编号 payment.account.name=账户名称 +payment.account.userName=User name payment.account.owner=账户拥有者姓名: payment.account.fullName=全称(名,中间名,姓) payment.account.state=州/省/地区 @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=电子邮箱或者电话号码 payment.venmo.venmoUserName=Venmo 用户名: payment.popmoney.accountId=电子邮箱或者电话号码 -payment.revolut.email=电子邮箱 -payment.revolut.phoneNr=注册的电话号码 payment.promptPay.promptPayId=公民身份证/税号或电话号码 payment.supportedCurrencies=支持的货币 payment.limitations=限制条件 @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=请确认您的银行允许您将现金存款汇入他人账户。例如,美国银行和富国银行不再允许此类存款。 -payment.revolut.info=请确保您用于您的 Revolut 账户的电话号码是注册在 Revolut 上的,否则 BTC 买家无法将资金发送给您。 +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=汇款单是 Bisq 上比较私人的法定货币购买方式之一。\n\n但是,请注意与它们的使用相关的潜在增加的风险。如果汇款单被盗, Bisq 将不承担任何责任,在这种情况下,调解员或仲裁员将把 BTC 判给汇款单的发送方,前提是他们能够提供跟踪信息和收据。寄件人最好在汇款单上写上卖方的名称,以减低汇款单被他人兑现的风险。 +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=联系方式 payment.f2f.contact.prompt=您希望如何与交易伙伴联系?(电子邮箱、电话号码、…) diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index e92e1523941..b7f3b410d8e 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -855,8 +855,13 @@ support.tab.mediation.support=調解 support.tab.arbitration.support=仲裁 support.tab.legacyArbitration.support=歷史仲裁 support.tab.ArbitratorsSupportTickets={0} 的工單 -support.filter=篩選列表 +support.filter=Search disputes support.filter.prompt=輸入 交易 ID、日期、洋蔥地址或賬戶資訊 +support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? +support.reOpenButton.label=Re-open dispute +support.sendNotificationButton.label=傳送私人通知 +support.reportButton.label=Generate report +support.fullReportButton.label=Get text dump of all disputes support.noTickets=沒有建立的話題 support.sendingMessage=傳送訊息... support.receiverNotOnline=收件人未線上。訊息被儲存到他們的郵箱。 @@ -898,7 +903,7 @@ support.youOpenedDisputeForMediation=您建立了一個調解請求。\n\n{0}\n\ support.peerOpenedTicket=對方因技術問題請求獲取幫助。\n\n{0}\n\nBisq 版本:{1} support.peerOpenedDispute=對方建立了一個糾紛請求。\n\n{0}\n\nBisq 版本:{1} support.peerOpenedDisputeForMediation=對方建立了一個調解請求。\n\n{0}\n\nBisq 版本:{1} -support.mediatorsDisputeSummary=系統訊息:\n調解糾紛總結:\n{0} +support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=仲裁員的節點地址:{0} @@ -976,7 +981,8 @@ settings.net.p2PPeersLabel=已連線節點 settings.net.onionAddressColumn=匿名地址 settings.net.creationDateColumn=已建立連線 settings.net.connectionTypeColumn=入/出 -settings.net.totalTrafficLabel=總流量: +settings.net.sentDataLabel=Sent data statistics +settings.net.receivedDataLabel=Received data statistics settings.net.roundTripTimeColumn=延遲 settings.net.sentBytesColumn=傳送 settings.net.receivedBytesColumn=接收 @@ -989,7 +995,8 @@ settings.net.heightColumn=高度 settings.net.needRestart=您需要重啟應用程式以同意這次變更。\n您需要現在重啟嗎? settings.net.notKnownYet=至今未知... -settings.net.sentReceived=傳送 {0},接收 {1} +settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec +settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec settings.net.ips=新增逗號分隔的 IP 地址及埠,如使用8333埠可不填寫。 settings.net.seedNode=種子節點 settings.net.directPeer=節點(直連) @@ -1011,8 +1018,8 @@ setting.about.support=支援 Bisq setting.about.def=Bisq 不是一個公司,而是一個社群項目,開放參與。如果您想參與或支援 Bisq,請點選下面連線。 setting.about.contribute=貢獻 setting.about.providers=資料提供商 -setting.about.apisWithFee=Bisq 使用第三方 API 獲取法定貨幣與虛擬幣的市場價以及礦工手續費的估價。 -setting.about.apis=Bisq 使用第三方 API 獲取法定貨幣與虛擬幣的市場價。 +setting.about.apisWithFee=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices, and Bisq Mempool Nodes for mining fee estimation. +setting.about.apis=Bisq uses Bisq Price Indices for Fiat and Altcoin market prices. setting.about.pricesProvided=交易所價格提供商 setting.about.feeEstimation.label=礦工手續費估算提供商 setting.about.versionDetails=版本詳情 @@ -1083,6 +1090,7 @@ setting.about.shortcuts.sendPrivateNotification.value=開啟在頭像或糾紛 account.tab.arbitratorRegistration=仲裁員註冊 account.tab.mediatorRegistration=調解員註冊 account.tab.refundAgentRegistration=退款助理註冊 +account.tab.signing=Signing account.tab.account=賬戶 account.info.headline=歡迎來到 Bisq 賬戶 account.info.msg=在這裡你可以設定交易賬戶的法定貨幣及數字貨幣,選擇仲裁員和備份你的錢包及賬戶資料。\n\n當你開始執行 Bisq 就已經建立了一個空的比特幣錢包。\n\n我們建議你在充值之前寫下你比特幣錢包的還原金鑰(在左邊的列表)和考慮新增密碼。在“資金”選項中管理比特幣存入和提現。\n\n隱私 & 安全:\nBisq 是一個去中心化的交易所 – 意味著您的所有資料都儲存在您的電腦上,沒有伺服器,我們無法訪問您的個人資訊,您的資金,甚至您的 IP 地址。如銀行賬號、數字貨幣、比特幣地址等資料只分享給與您交易的人,以實現您發起的交易(如果有爭議,仲裁員將會看到您的交易資料)。 @@ -1947,17 +1955,37 @@ disputeSummaryWindow.payoutAmount.buyer=買家支付金額 disputeSummaryWindow.payoutAmount.seller=賣家支付金額 disputeSummaryWindow.payoutAmount.invert=使用失敗者作為釋出者 disputeSummaryWindow.reason=糾紛的原因 -disputeSummaryWindow.reason.bug=Bug -disputeSummaryWindow.reason.usability=可用性 -disputeSummaryWindow.reason.protocolViolation=違反協議 -disputeSummaryWindow.reason.noReply=不回覆 -disputeSummaryWindow.reason.scam=詐騙 -disputeSummaryWindow.reason.other=其他 -disputeSummaryWindow.reason.bank=銀行 + +# dynamic values are not recognized by IntelliJ +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BUG=Bug +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.USABILITY=可用性 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PROTOCOL_VIOLATION=違反協議 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.NO_REPLY=不回覆 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SCAM=詐騙 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OTHER=其他 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.BANK_PROBLEMS=銀行 +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.OPTION_TRADE=Option trade +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.SELLER_NOT_RESPONDING=Seller not responding +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.WRONG_SENDER_ACCOUNT=Wrong sender account +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.PEER_WAS_LATE=Peer was late +# suppress inspection "UnusedProperty" +disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled + disputeSummaryWindow.summaryNotes=總結說明 disputeSummaryWindow.addSummaryNotes=新增總結說明 disputeSummaryWindow.close.button=關閉話題 -disputeSummaryWindow.close.msg=工單已關閉 {0}\n\n摘要:\nBTC 買家的支付金額:{1}\nBTC 賣家的支付金額:{2}\n\n總結說明:\n{3} +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\nSummary:\nPayout amount for BTC buyer: {1}\nPayout amount for BTC seller: {2}\n\nReason for dispute: {3}\n\nSummary notes:\n{4} disputeSummaryWindow.close.nextStepsForMediation=\n\n下一個步驟:\n開啟未完成交易,接受或拒絕建議的調解員的建議 disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\n下一個步驟:\n不需要您採取進一步的行動。如果仲裁員做出了對你有利的裁決,你將在 資金/交易 頁中看到“仲裁退款”交易 disputeSummaryWindow.close.closePeer=你也需要關閉交易物件的話題! @@ -1985,7 +2013,8 @@ filterWindow.onions=篩選匿名地址(用逗號“,”隔開) filterWindow.accounts=篩選交易賬戶資料:\n格式:逗號分割的 [付款方式ID|資料欄位|值] filterWindow.bannedCurrencies=篩選貨幣程式碼(用逗號“,”隔開) filterWindow.bannedPaymentMethods=篩選支付方式 ID(用逗號“,”隔開) -filterWindow.bannedSignerPubKeys=Filtered signer pubkeys (comma sep. hex of pubkeys) +filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) +filterWindow.bannedPrivilegedDevPubKeys=Filtered privileged dev pub keys (comma sep. hex of pub keys) filterWindow.arbitrators=篩選後的仲裁人(用逗號“,”隔開的洋蔥地址) filterWindow.mediators=篩選後的調解員(用逗號“,”隔開的洋蔥地址) filterWindow.refundAgents=篩選後的退款助理(用逗號“,”隔開的洋蔥地址) @@ -2140,7 +2169,7 @@ popup.warning.insufficientBtcFundsForBsqTx=你沒有足夠的 BTC 資金支付 popup.warning.bsqChangeBelowDustException=該交易產生的 BSQ 變化輸出低於零頭限制(5.46 BSQ),將被比特幣網路拒絕。\n\n您需要傳送更高的金額以避免更改輸出(例如,通過在您的傳送金額中新增零頭),或者向您的錢包中新增更多的 BSQ 資金,以避免生成零頭輸出。\n\n零頭輸出為 {0}。 popup.warning.btcChangeBelowDustException=該交易建立的更改輸出低於零頭限制(546 聰),將被比特幣網路拒絕。\n\n您需要將零頭新增到傳送量中,以避免生成零頭輸出。\n\n零頭輸出為{0}。 -popup.warning.insufficientBsqFundsForBtcFeePayment=您沒有足夠的 BSQ 資金支付 BSQ 的交易費用。您可以在 BTC 支付費用,或者您需要充值您的 BSQ 錢包。你可以在 Bisq 買到 BSQ 。\n\n缺少 BSQ 資金:{0} +popup.warning.insufficientBsqFundsForBtcFeePayment=You''ll need more BSQ to do this transaction—the last 5.46 BSQ in your wallet cannot be used to pay trade fees because of dust limits in the Bitcoin protocol.\n\nYou can either buy more BSQ or pay trade fees with BTC.\n\nMissing funds: {0} popup.warning.noBsqFundsForBtcFeePayment=您的 BSQ 錢包沒有足夠的資金支付 BSQ 的交易費用。 popup.warning.messageTooLong=您的資訊超過最大允許的大小。請將其分成多個部分發送,或將其上傳到 https://pastebin.com 之類的伺服器。 popup.warning.lockedUpFunds=你已經從一個失敗的交易中凍結了資金。\n凍結餘額:{0}\n存款tx地址:{1}\n交易單號:{2}\n\n請通過選擇待處理交易介面中的交易並點選“alt + o”或“option+ o”開啟幫助話題。 @@ -2437,6 +2466,7 @@ seed.restore.error=使用還原金鑰恢復錢包時出現錯誤。{0} payment.account=賬戶 payment.account.no=賬戶編號 payment.account.name=賬戶名稱 +payment.account.userName=User name payment.account.owner=賬戶擁有者姓名: payment.account.fullName=全稱(名,中間名,姓) payment.account.state=州/省/地區 @@ -2468,8 +2498,6 @@ payment.cashApp.cashTag=$Cashtag payment.moneyBeam.accountId=電子郵箱或者電話號碼 payment.venmo.venmoUserName=Venmo 使用者名稱: payment.popmoney.accountId=電子郵箱或者電話號碼 -payment.revolut.email=電子郵箱 -payment.revolut.phoneNr=註冊的電話號碼 payment.promptPay.promptPayId=公民身份證/稅號或電話號碼 payment.supportedCurrencies=支援的貨幣 payment.limitations=限制條件 @@ -2518,9 +2546,11 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li payment.cashDeposit.info=請確認您的銀行允許您將現金存款匯入他人賬戶。例如,美國銀行和富國銀行不再允許此類存款。 -payment.revolut.info=請確保您用於您的 Revolut 賬戶的電話號碼是註冊在 Revolut 上的,否則 BTC 買家無法將資金髮送給您。 +payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past. +payment.account.revolut.addUserNameInfo={0}\nYour existing Revolut account ({1}) does not has set the ''User name''.\nPlease enter your Revolut ''User name'' to update your account data.\nThis will not affect your account age signing status. +payment.revolut.addUserNameInfo.headLine=Update Revolut account -payment.usPostalMoneyOrder.info=匯款單是 Bisq 上比較私人的法定貨幣購買方式之一。\n\n但是,請注意與它們的使用相關的潛在增加的風險。如果匯款單被盜, Bisq 將不承擔任何責任,在這種情況下,調解員或仲裁員將把 BTC 判給匯款單的傳送方,前提是他們能夠提供跟蹤資訊和收據。寄件人最好在匯款單上寫上賣方的名稱,以減低匯款單被他人兌現的風險。 +payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\n- BTC buyers must write the BTC Seller’s name in both the Payer and the Payee’s fields & take a high-resolution photo of the USPMO and envelope with proof of tracking before sending.\n- BTC buyers must send the USPMO to the BTC seller with Delivery Confirmation.\n\nIn the event mediation is necessary, or if there is a trade dispute, you will be required to send the photos to the Bisq mediator or refund agent, together with the USPMO Serial Number, Post Office Number, and dollar amount, so they can verify the details on the US Post Office website.\n\nFailure to provide the required information to the Mediator or Arbitrator will result in losing the dispute case.\n\nIn all dispute cases, the USPMO sender bears 100% of the burden of responsibility in providing evidence/proof to the Mediator or Arbitrator.\n\nIf you do not understand these requirements, do not trade using USPMO on Bisq. payment.f2f.contact=聯絡方式 payment.f2f.contact.prompt=您希望如何與交易夥伴聯絡?(電子郵箱、電話號碼、…) diff --git a/core/src/test/java/bisq/core/network/p2p/FileDatabaseTest.java b/core/src/test/java/bisq/core/network/p2p/FileDatabaseTest.java new file mode 100644 index 00000000000..272da6f56aa --- /dev/null +++ b/core/src/test/java/bisq/core/network/p2p/FileDatabaseTest.java @@ -0,0 +1,331 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p; + +import bisq.core.account.witness.AccountAgeWitness; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for migrating to and operating a multi-file file-based data store system + * + * TODO these tests are bound to be changed once Bisq migrates to a real database backend + */ +public class FileDatabaseTest extends FileDatabaseTestUtils { + + /** + * TEST CASE: check if test fixture databases are in place and correct

+ * + * This does not test any business logic, just makes sure the test setup is correct. + */ + @Test + public void checkTestFixtures() throws Exception { + checkTestFixturesHelper(object1); + checkTestFixturesHelper(object1, object2); + checkTestFixturesHelper(object2); + checkTestFixturesHelper(object2, object3); + checkTestFixturesHelper(object3); + } + + private void checkTestFixturesHelper(AccountAgeWitness... objects) throws Exception { + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), objects); + createDatabase(createFile(false, "AccountAgeWitnessStore"), objects); + AppendOnlyDataStoreService DUT = loadDatabase(); + Assert.assertEquals(objects.length, DUT.getMap().size()); + Arrays.stream(objects).forEach(object -> { + Assert.assertTrue(DUT.getMap().containsValue(object)); + }); + } + + /** + * TEST CASE: test migration scenario from old database file model to new one

+ * + * USE CASE: + * We migrate from just having one working-dir database file to having multiple. In + * detail, the user starts with having one database file in her working directory and + * one in her resources. After the update, there is still one database in her resources + * but it is labelled differently.

+ * + * RESULT: + * There are 2 data stores in her working directory, one holding the live database, + * the other one being the exact and readonly copy of the database in resources. Plus, + * the 2 data stores do not share any set of objects. + */ + @Test + public void migrationScenario() throws Exception { + // setup scenario + // - create one data store in working directory + createDatabase(createFile(false, "AccountAgeWitnessStore"), object1, object2); + // - create one data store in resources with new naming scheme + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(0) + "_TEST"), object1); + + // simulate bisq startup + final AppendOnlyDataStoreService DUT = loadDatabase(); + + // check result + // - check total number of elements + Assert.assertEquals(2, DUT.getMap().size()); + // - are there 2 data stores in working-dir + Assert.assertEquals(2, storageDir.list((dir, name) -> name.startsWith("AccountAgeWitnessStore") && !name.endsWith("_TEST")).length); + // - do the 2 data stores share objects + Assert.assertEquals(1, DUT.getMap("since " + getVersion(0)).size()); + Assert.assertEquals(1, DUT.getMap().size() - DUT.getMap("since " + getVersion(0)).size()); + } + + /** + * TEST CASE: test migration scenario from old database file model to new one but the + * user skipped some releases before upgrading

+ * + * USE CASE: + * We migrate from just having one working-dir database file to having multiple. In + * detail, the user starts with having one database file in her working directory and + * multiple data store files in her resources. This can happen if the user does not + * upgrade at the first possible moment. After the update, however, there is all of the + * resource files copied to her working directory plus the live data store.

+ * + * RESULT: + * There are 2 data stores in her working directory, one holding the live database, + * the other one being the exact and readonly copy of the database in resources. Plus, + * the 2 data stores do not share any set of objects. + */ + @Test + public void migrationScenario2() throws Exception { + // setup scenario + // - create one data store in working directory + createDatabase(createFile(false, "AccountAgeWitnessStore"), object1, object2); + // - create one data store in resources with new naming scheme + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(-1) + "_TEST"), object1); + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(0) + "_TEST"), object2); + + // simulate bisq startup + final AppendOnlyDataStoreService DUT = loadDatabase(); + + // check result + // - check total number of elements + Assert.assertEquals(2, DUT.getMap().size()); + // - are there 2 data stores in working-dir + Assert.assertEquals(3, storageDir.list((dir, name) -> name.startsWith("AccountAgeWitnessStore") && !name.endsWith("_TEST")).length); + // - do the 2 data stores share objects + Assert.assertEquals(0, DUT.getMap("since " + getVersion(0)).size()); + Assert.assertEquals(1, DUT.getMap("since " + getVersion(-1)).size()); + } + + /** + * TEST CASE: test Bisq software update scenario

+ * + * USE CASE: + * Given a newly released Bisq version x, a new differential database is added to + * the resources. The new database has to be copied to the working directory and the + * live database has to be stripped of the entries found in the new database.

+ * + * RESULT: + * There are n + 1 data stores in the users working directory, one holding the live + * database, the other 2 being the exact and readonly copy of the data stores in + * resources. Plus, the data stores in the working dir do not share any set of objects. + */ + @Test + public void updateScenario() throws Exception { + // setup scenario + // - create two data stores in resources + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(-1) + "_TEST"), object1); + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(0) + "_TEST"), object2); + // - create two data stores in work dir + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(-1)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object2, object3); + + // simulate bisq startup + AppendOnlyDataStoreService DUT = loadDatabase(); + + // check result + // - check total number of elements + Assert.assertEquals(3, DUT.getMap().size()); + // - are there 2 data stores in working-dir + Assert.assertEquals(3, storageDir.list((dir, name) -> name.startsWith("AccountAgeWitnessStore") && !name.endsWith("_TEST")).length); + // - do the 2 data stores share objects + Assert.assertEquals(1, DUT.getMap("since " + getVersion(0)).size()); + Assert.assertEquals(2, DUT.getMap().size() - DUT.getMap("since " + getVersion(0)).size()); + } + + /** + * TEST CASE: test clean install of Bisq software app

+ * + * USE CASE: + * A user has a fresh install of Bisq. Ie. there are two database files in resources + * and none in the working directory.

+ * + * RESULT: + * After startup, there should be 3 data stores in the working directory. + */ + @Test + public void freshInstallScenario() throws Exception { + // setup scenario + // - create two data stores in resources + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(-1) + "_TEST"), object1); + createDatabase(createFile(true, "AccountAgeWitnessStore_" + getVersion(0) + "_TEST"), object2); + + // simulate bisq startup + AppendOnlyDataStoreService DUT = loadDatabase(); + + // check result + // - check total number of elements + Assert.assertEquals(2, DUT.getMap().size()); + // - are there 2 data stores in working-dir + Assert.assertEquals(3, storageDir.list((dir, name) -> name.startsWith("AccountAgeWitnessStore") && !name.endsWith("_TEST")).length); + // - do the 3 data stores share objects + Assert.assertEquals(0, DUT.getMap("since " + getVersion(0)).size()); + Assert.assertEquals(1, DUT.getMap("since " + getVersion(-1)).size()); + Assert.assertEquals(2, DUT.getMap("since " + getVersion(-2)).size()); + } + + /** + * TEST CASE: test if getMap still return all elements

+ * + * USE CASE: + * The app needs all data for . Currently, this is the most + * encountered use case. Future plans, however, aim to reduce the need for this use + * case.

+ * + * RESULT: + * getMap returns all elements stored in the various database files. + */ + @Test + public void getMap() throws Exception { + // setup scenario + // - create 2 data stores containing historical data and a live database + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(-1)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object2); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object3); + + // simulate Bisq startup + AppendOnlyDataStoreService DUT = loadDatabase(); + + // check result + // - getMap still gets all the objects? + Assert.assertEquals(3, DUT.getMap().size()); + Assert.assertTrue(DUT.getMap().containsValue(object1)); + Assert.assertTrue(DUT.getMap().containsValue(object2)); + Assert.assertTrue(DUT.getMap().containsValue(object3)); + } + + /** + * TEST CASE: test if getMap filtering works

+ * + * USE CASE: + * After introducing the database snapshot functionality, the app only requests objects + * which it got since the last database snapshot.

+ * + * RESULT: + * getMap(since x) returns all elements added after snapshot x + */ + @Test + public void getMapSinceFilter() throws Exception { + // setup scenario + // - create 2 data stores containing historical data and a live database + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(-1)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object2); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object3); + + // simulate bisq startup + AppendOnlyDataStoreService DUT = loadDatabase(); + + // check result + // - check "live" filter + Collection result = DUT.getMap("since " + getVersion(0)).values(); + Assert.assertEquals(1, result.size()); + Assert.assertTrue(result.contains(object3)); + // - check "since version" filter + result = DUT.getMap("since " + getVersion(-1)).values(); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(object2)); + Assert.assertTrue(result.contains(object3)); + } + + /** + * TEST CASE: test if adding new data only adds to live database

+ * + * USE CASE: + * Whenever new objects come in, they are going to be persisted to disk so that + * the local database is kept in sync with the distributed database.

+ * + * RESULT: + * map.put should only add data to the live database. Other data stores are read-only! + */ + @Test + public void put() throws Exception { + // setup scenario + // - create one database containing historical data and a live database + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object2); + + // simulate Bisq startup + AppendOnlyDataStoreService DUT = loadDatabase(); + // add data + DUT.put(new P2PDataStorage.ByteArray(object3.getHash()), object3); + + // check result + // - did the live database grow? + Collection result = DUT.getMap("since " + getVersion(0)).values(); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(object2)); + Assert.assertTrue(result.contains(object3)); + // - did the historical data data store grow? + Assert.assertEquals(1, DUT.getMap().size() - result.size()); + } + + /** + * TEST CASE: test if only new data is added given a set of data we partially already + * know.

+ * + * USE CASE: + * Given a Bisq client version x asks a seed node version x-1 for data, it might receive + * data it already has. We do not want to add duplicates to our local database.

+ * + * RESULT: + * Check for duplicates + */ + @Test + public void putDuplicates() throws Exception { + // setup scenario + // - create one database containing historical data and a live database + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object2); + + // simulate Bisq startup + AppendOnlyDataStoreService DUT = loadDatabase(); + // add data + // - duplicate data + DUT.put(new P2PDataStorage.ByteArray(object1.getHash()), object1); + // - legit data + DUT.put(new P2PDataStorage.ByteArray(object3.getHash()), object3); + + // check result + // - did the live database grow? + Collection result = DUT.getMap("since " + getVersion(0)).values(); + Assert.assertEquals(2, result.size()); + Assert.assertTrue(result.contains(object2)); + Assert.assertTrue(result.contains(object3)); + } +} diff --git a/core/src/test/java/bisq/core/network/p2p/FileDatabaseTestUtils.java b/core/src/test/java/bisq/core/network/p2p/FileDatabaseTestUtils.java new file mode 100644 index 00000000000..416bc1a4c11 --- /dev/null +++ b/core/src/test/java/bisq/core/network/p2p/FileDatabaseTestUtils.java @@ -0,0 +1,129 @@ +package bisq.core.network.p2p; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.account.witness.AccountAgeWitnessStorageService; +import bisq.core.account.witness.AccountAgeWitnessStore; +import bisq.core.proto.persistable.CorePersistenceProtoResolver; + +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; + +import bisq.common.app.Version; +import bisq.common.storage.Storage; + +import java.nio.file.Files; +import java.nio.file.Path; + +import java.io.File; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import org.junit.After; + +/** + * utility class for the {@link FileDatabaseTest} and {@link RequestDataTest} + */ +public class FileDatabaseTestUtils { + // Test fixtures + static final AccountAgeWitness object1 = new AccountAgeWitness(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 1); + static final AccountAgeWitness object2 = new AccountAgeWitness(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, 2); + static final AccountAgeWitness object3 = new AccountAgeWitness(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3}, 3); + + + List files = new ArrayList<>(); + static final File storageDir = new File("src/test/resources"); + + @After + public void cleanup() { + waitForFile(); + + for (File file : files) { + file.delete(); + } + + try { + File backupDir = new File(storageDir + "/backup"); + if (backupDir.exists()) + Files.walk(backupDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } catch (IOException e) { + e.printStackTrace(); + } + + Arrays.stream(storageDir.list((dir, name) -> name.startsWith("AccountAgeWitnessStore"))).forEach(s -> { + new File(storageDir + File.separator + s).delete(); + }); + } + + public File createFile(boolean isResourceFile, String name) { + File tmp; + if (isResourceFile) + tmp = new File(ClassLoader.getSystemClassLoader().getResource("").getFile() + File.separator + name); + else + tmp = new File(storageDir + File.separator + name); + + files.add(tmp); + return tmp; + } + + public void waitForFile() { + try { + boolean done = false; + while (!done) { + Thread.sleep(100); + Set threads = Thread.getAllStackTraces().keySet(); + done = threads.stream().noneMatch(thread -> thread.getName().startsWith("Save-file-task")); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + protected AppendOnlyDataStoreService loadDatabase() { + Storage storage = new Storage<>(storageDir, new CorePersistenceProtoResolver(null, null, null, null), null); + AccountAgeWitnessStorageService storageService = new AccountAgeWitnessStorageService(storageDir, storage); + final AppendOnlyDataStoreService protectedDataStoreService = new AppendOnlyDataStoreService(); + protectedDataStoreService.addService(storageService); + protectedDataStoreService.readFromResources("_TEST"); + + waitForFile(); + + return protectedDataStoreService; + } + + protected void createDatabase(File target, + AccountAgeWitness... objects) throws IOException { + + if (null == objects) { + return; + } + + String filename = ""; + filename += Arrays.asList(objects).contains(object1) ? "o1" : ""; + filename += Arrays.asList(objects).contains(object2) ? "o2" : ""; + filename += Arrays.asList(objects).contains(object3) ? "o3" : ""; + + File source = new File(storageDir + File.separator + filename); + + if (target.exists()) + target.delete(); + + Files.copy(source.toPath(), target.toPath()); + } + + /** + * note that this function assumes a Bisq version format of x.y.z. It will not work with formats other than that eg. x.yy.z + * @param offset + * @return version string relative to v1.3.7 + */ + public String getVersion(int offset) throws Exception { + String result = new StringBuilder().append(Integer.valueOf(Version.VERSION.replace(".", "")) + offset).insert(2, ".").insert(1, ".").toString(); + + assert result.equals(Version.VERSION) | Version.history.contains(result) : "A test in FileDatabaseTest requested a version which is not included in the test environment."; + + return result; + } +} diff --git a/core/src/test/java/bisq/core/network/p2p/RequestDataTest.java b/core/src/test/java/bisq/core/network/p2p/RequestDataTest.java new file mode 100644 index 00000000000..7b69ef6fe48 --- /dev/null +++ b/core/src/test/java/bisq/core/network/p2p/RequestDataTest.java @@ -0,0 +1,235 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network.p2p; + +import bisq.core.account.witness.AccountAgeWitness; + +import bisq.network.p2p.network.LocalhostNetworkNode; +import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.persistence.ProtectedDataStoreService; +import bisq.network.p2p.storage.persistence.SequenceNumberMap; + +import bisq.common.app.Version; +import bisq.common.storage.Storage; + +import java.io.File; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for the "reduced" initial query size feature. See + * project description on github. + */ +public class RequestDataTest extends FileDatabaseTestUtils { + + /** + * TEST CASE: test the optimized query creation

+ * + * USE CASE: + * In order to save on data to be transmitted, we summarize data history by reporting + * our bisq version as a "special key". The queried peers then know which data we + * already have.

+ * + * RESULT: + * Test whether our client creates the correct query. + */ + @Test + public void query() throws Exception { + // setup scenario + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(-1)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object2); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object3); + + // create a PreliminaryGetDataRequest as a Device Under Test + P2PDataStorage DUT = new P2PDataStorage(new LocalhostNetworkNode(9999, null), null, loadDatabase(), new ProtectedDataStoreService(), null, new SequenceNumberStorageFake(), null, null, 0); + Set result = DUT.buildPreliminaryGetDataRequest(0).getExcludedKeys().stream().map(bytes -> new P2PDataStorage.ByteArray(bytes)).collect(Collectors.toSet()); + + // check result + // - check total number of elements + Assert.assertEquals(2, result.size()); + // - check keys + Assert.assertFalse(result.contains(new P2PDataStorage.ByteArray(object1.getHash()))); + Assert.assertFalse(result.contains(new P2PDataStorage.ByteArray(object2.getHash()))); + Assert.assertTrue(result.contains(new P2PDataStorage.ByteArray(object3.getHash()))); + // - check special key + Assert.assertTrue(result.contains(new P2PDataStorage.ByteArray(getSpecialKey(getVersion(0)).getHash()))); + } + + /** + * TEST CASE: test the optimized query evaluation

+ * + * USE CASE: + * In order to save on data to be transmitted, we summarize data history by reporting + * our bisq version as a "special key". The queried peers then know which data we + * already have.

+ * + * RESULT: + * Given the new query, see if another peer would respond correctly + */ + @Test + public void response() throws Exception { + // setup scenario + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(-1)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object2); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object3); + + // craft a PreliminaryGetDataRequest as a query to get a GetDataResponse + P2PDataStorage DUT = new P2PDataStorage(new LocalhostNetworkNode(9999, null), null, loadDatabase(), new ProtectedDataStoreService(), null, new SequenceNumberStorageFake(), null, null, 0); + PreliminaryGetDataRequest query = new PreliminaryGetDataRequest(0, new HashSet<>(Arrays.asList(getSpecialKey(getVersion(0)).getHash()))); + Set result = DUT.buildGetDataResponse(query, 100000, null, null, null).getPersistableNetworkPayloadSet(); + + // check result + // - check total number of elements + Assert.assertEquals(1, result.size()); + // - check keys + Assert.assertFalse(result.contains(object1)); + Assert.assertFalse(result.contains(object2)); + Assert.assertTrue(result.contains(object3)); + + // alter query slightly + query = new PreliminaryGetDataRequest(0, new HashSet<>(Arrays.asList(getSpecialKey(getVersion(-1)).getHash(), object3.getHash()))); + result = DUT.buildGetDataResponse(query, 100000, null, null, null).getPersistableNetworkPayloadSet(); + + // check result + // - check total number of elements + Assert.assertEquals(1, result.size()); + // - check keys + Assert.assertFalse(result.contains(object1)); + Assert.assertTrue(result.contains(object2)); + Assert.assertFalse(result.contains(object3)); + } + + /** + * TEST CASE: what happens if a faulty key arrives

+ * + * USE CASE: + * An Attacker tries to hijack the protocol by sending a incorrect "special key". The + * system should be resilient against that and thus, recover and fall back to a known + * state.

+ * + * RESULT: + * Although it would be nice to have some sort of detection, it is hard to do right. + * So we just stick to what happened until now, namely, we ignore the special key + * and let other size limits do the work.

+ * + * However, we add an additional test to ensure Bisq version follows a strict pattern. + */ + @Test + public void faultySpecialKey() throws Exception { + // setup scenario + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(0)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object2); + + // craft a PreliminaryGetDataRequest as a query to get a GetDataResponse + P2PDataStorage DUT = new P2PDataStorage(new LocalhostNetworkNode(9999, null), null, loadDatabase(), new ProtectedDataStoreService(), null, new SequenceNumberStorageFake(), null, null, 0); + + // check results + List faultyKeys = Arrays.asList("1.3.13", "1.13.3", "13.3.3", "a.3.3", "13.a.3", "ä.3.3", Version.VERSION.replace(".", "_"), Version.VERSION.replace(".", ",")); + faultyKeys.forEach(s -> faultySpecialKeyHelper(DUT, s)); + } + + private void faultySpecialKeyHelper(P2PDataStorage DUT, String key) { + PreliminaryGetDataRequest query = new PreliminaryGetDataRequest(0, new HashSet<>(Arrays.asList(getSpecialKey(key).getHash()))); + + Set result = DUT.buildGetDataResponse(query, 100000, null, null, null).getPersistableNetworkPayloadSet(); + Assert.assertEquals(key + " got accepted", 2, result.size()); + Assert.assertTrue(key + " got accepted", result.contains(object1)); + Assert.assertTrue(key + " got accepted", result.contains(object2)); + } + + /** + * TEST CASE: make sure Bisq follows a strict pattern in release versioning.

+ * + * USE CASE: + * Bringing in the timely element we need to make sure Bisq's release versioning stays + * true to the pattern it follows now.

+ * + * RESULT: + * If the pattern does not match, we fire! + */ + @Test + public void testBisqVersionFormat() { + Assert.assertTrue("Bisq Version does not match formatting! x.y.z vs. " + Version.VERSION, Version.VERSION.matches("^[0-9]\\.[0-9]\\.[0-9]$")); + } + + /** + * TEST CASE: test what happens if an incoming "special key" is a future bisq version

+ * + * USE CASE: + * A Bisq client Alice of version x asks a Bisq client Bob of version x-1 for data. + * The "special" key incoming to Bob describes its future. A real-world example is an + * outdated seednode getting asked by an up-to-date client.

+ * + * RESULT: + * send anything new since the seednode's newest data bucket - ie. all live data. This + * will result in duplicates arriving at the client but + *
  • we took care of that already
  • + *
  • is a sane way of reacting to a newer client
+ */ + @Test + public void futureSpecialKey() throws Exception { + // setup scenario + createDatabase(createFile(false, "AccountAgeWitnessStore_" + getVersion(-1)), object1); + createDatabase(createFile(false, "AccountAgeWitnessStore"), object2, object3); + + // craft a PreliminaryGetDataRequest as a query to get a GetDataResponse + P2PDataStorage DUT = new P2PDataStorage(new LocalhostNetworkNode(9999, null), null, loadDatabase(), new ProtectedDataStoreService(), null, new SequenceNumberStorageFake(), null, null, 0); + PreliminaryGetDataRequest query = new PreliminaryGetDataRequest(0, new HashSet<>(Arrays.asList(getSpecialKey(getVersion(0)).getHash()))); + Set result = DUT.buildGetDataResponse(query, 100000, null, null, null).getPersistableNetworkPayloadSet(); + + // check result + // - check total number of elements + Assert.assertEquals(2, result.size()); + // - check keys + Assert.assertFalse(result.contains(object1)); + Assert.assertTrue(result.contains(object2)); + Assert.assertTrue(result.contains(object3)); + } + + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////// Utils ///////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + + public class SequenceNumberStorageFake extends Storage { + + public SequenceNumberStorageFake() { + super(new File("src/test/resources"), null, null); + } + + @Override + public void setNumMaxBackupFiles(int numMaxBackupFiles) { + // left empty intentionally + } + } + + public static AccountAgeWitness getSpecialKey(String version) { + byte[] result = new byte[20]; + Arrays.fill(result, (byte) 0); + System.arraycopy(version.getBytes(), 0, result, 0, version.length()); + return new AccountAgeWitness(result, 0); + } +} diff --git a/core/src/test/resources/o1 b/core/src/test/resources/o1 new file mode 100644 index 00000000000..0ae8fd6a236 Binary files /dev/null and b/core/src/test/resources/o1 differ diff --git a/core/src/test/resources/o1o2 b/core/src/test/resources/o1o2 new file mode 100644 index 00000000000..e53dace8c19 Binary files /dev/null and b/core/src/test/resources/o1o2 differ diff --git a/core/src/test/resources/o2 b/core/src/test/resources/o2 new file mode 100644 index 00000000000..f816dfe4faa Binary files /dev/null and b/core/src/test/resources/o2 differ diff --git a/core/src/test/resources/o2o3 b/core/src/test/resources/o2o3 new file mode 100644 index 00000000000..5696c609447 Binary files /dev/null and b/core/src/test/resources/o2o3 differ diff --git a/core/src/test/resources/o3 b/core/src/test/resources/o3 new file mode 100644 index 00000000000..655c449851c Binary files /dev/null and b/core/src/test/resources/o3 differ diff --git a/desktop/package/linux/Dockerfile b/desktop/package/linux/Dockerfile index b0c1bd474b6..1838bc7a537 100644 --- a/desktop/package/linux/Dockerfile +++ b/desktop/package/linux/Dockerfile @@ -8,7 +8,7 @@ # pull base image FROM openjdk:8-jdk -ENV version 1.3.7 +ENV version 1.3.8 RUN apt-get update && apt-get install -y --no-install-recommends openjfx && rm -rf /var/lib/apt/lists/* && apt-get install -y vim fakeroot diff --git a/desktop/package/linux/package.sh b/desktop/package/linux/package.sh index 61975412a32..0e395990544 100755 --- a/desktop/package/linux/package.sh +++ b/desktop/package/linux/package.sh @@ -6,7 +6,7 @@ # - Update version below # - Ensure JAVA_HOME below is pointing to OracleJDK 10 directory -version=1.3.7 +version=1.3.8 version_base=$(echo $version | awk -F'[_-]' '{print $1}') if [ ! -f "$JAVA_HOME/bin/javapackager" ]; then if [ -d "/usr/lib/jvm/jdk-10.0.2" ]; then diff --git a/desktop/package/linux/release.sh b/desktop/package/linux/release.sh index 0eefb6a521f..3ba92885a0b 100755 --- a/desktop/package/linux/release.sh +++ b/desktop/package/linux/release.sh @@ -4,7 +4,7 @@ # Prior to running this script: # - Update version below -version=1.3.7 +version=1.3.8 base_dir=$( cd "$(dirname "$0")" ; pwd -P )/../../.. package_dir=$base_dir/desktop/package release_dir=$base_dir/desktop/release/$version diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 7125f93a83d..bcdbedce14e 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.3.7 + 1.3.8 CFBundleShortVersionString - 1.3.7 + 1.3.8 CFBundleExecutable Bisq diff --git a/desktop/package/macosx/create_app.sh b/desktop/package/macosx/create_app.sh index 2938ac7bcd2..57b0cb4bda8 100755 --- a/desktop/package/macosx/create_app.sh +++ b/desktop/package/macosx/create_app.sh @@ -6,7 +6,7 @@ mkdir -p deploy set -e -version="1.3.7" +version="1.3.8" cd .. ./gradlew :desktop:build -x test shadowJar diff --git a/desktop/package/macosx/finalize.sh b/desktop/package/macosx/finalize.sh index 16ed7856ce1..59df06eaa1c 100755 --- a/desktop/package/macosx/finalize.sh +++ b/desktop/package/macosx/finalize.sh @@ -2,7 +2,7 @@ cd ../../ -version="1.3.7" +version="1.3.8" target_dir="releases/$version" diff --git a/desktop/package/macosx/insert_snapshot_version.sh b/desktop/package/macosx/insert_snapshot_version.sh index 9d365421a3e..4e214a5581a 100755 --- a/desktop/package/macosx/insert_snapshot_version.sh +++ b/desktop/package/macosx/insert_snapshot_version.sh @@ -2,7 +2,7 @@ cd $(dirname $0)/../../../ -version=1.3.6 +version=1.3.7 find . -type f \( -name "finalize.sh" \ -o -name "create_app.sh" \ diff --git a/desktop/package/macosx/replace_version_number.sh b/desktop/package/macosx/replace_version_number.sh index 9f21dcae758..33f30a5440d 100755 --- a/desktop/package/macosx/replace_version_number.sh +++ b/desktop/package/macosx/replace_version_number.sh @@ -2,8 +2,8 @@ cd $(dirname $0)/../../../ -oldVersion=1.3.6 -newVersion=1.3.7 +oldVersion=1.3.7 +newVersion=1.3.8 find . -type f \( -name "finalize.sh" \ -o -name "create_app.sh" \ diff --git a/desktop/package/windows/package.bat b/desktop/package/windows/package.bat index 6103e8ebbe6..7f4787f5a97 100644 --- a/desktop/package/windows/package.bat +++ b/desktop/package/windows/package.bat @@ -11,7 +11,7 @@ @echo off -set version=1.3.7 +set version=1.3.8 if not exist "%JAVA_HOME%\bin\javapackager.exe" ( if not exist "%ProgramFiles%\Java\jdk-10.0.2" ( echo Javapackager not found. Update JAVA_HOME variable to point to OracleJDK. diff --git a/desktop/package/windows/release.bat b/desktop/package/windows/release.bat index 6b4b09ed0af..f6b98fbc5f4 100644 --- a/desktop/package/windows/release.bat +++ b/desktop/package/windows/release.bat @@ -6,7 +6,7 @@ @echo off -set version=1.3.7 +set version=1.3.8 set release_dir=%~dp0..\..\..\releases\%version% set package_dir=%~dp0.. diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index e9ab97167cf..114f272eda1 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -436,8 +436,10 @@ tree-table-view:focused { -fx-pref-width: 30; } -.jfx-badge.autoconf .badge-pane { - -fx-pref-width: 100; +.jfx-badge.auto-conf .badge-pane { + -fx-background-color: -xmr-orange; + -fx-pref-width: -1; + -fx-padding: -1 10 0 10; } .jfx-badge .badge-pane .label { @@ -814,6 +816,11 @@ tree-table-view:focused { -fx-padding: 27 2 0 2; } +.alert-icon { + -fx-fill: -bs-rd-error-red; + -fx-cursor: hand; +} + .close-icon { -fx-fill: -bs-text-color; } @@ -856,6 +863,12 @@ textfield */ -fx-max-height: 20; } +#xmr-confidence { + -fx-progress-color: -xmr-orange; + -fx-max-width: 20; + -fx-max-height: 20; +} + .hyperlink, .hyperlink.force-underline .text, .hyperlink:hover, diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java index 6ad54d94333..8ed82f821e0 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java @@ -32,15 +32,20 @@ import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; +import bisq.common.util.Tuple2; + import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; +import lombok.extern.slf4j.Slf4j; + import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +@Slf4j public class RevolutForm extends PaymentMethodForm { private final RevolutAccount account; private RevolutValidator validator; @@ -48,9 +53,8 @@ public class RevolutForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { - String userName = ((RevolutAccountPayload) paymentAccountPayload).getUserName(); - addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), userName); - + Tuple2 tuple = ((RevolutAccountPayload) paymentAccountPayload).getRecipientsAccountData(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, tuple.first, tuple.second); return gridRow; } @@ -104,9 +108,17 @@ public void addFormForDisplayAccount() { account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); + String userName = account.getUserName(); - TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; - field.setMouseTransparent(false); + TextField userNameTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; + userNameTf.setMouseTransparent(false); + + if (account.hasOldAccountId()) { + String accountId = account.getAccountId(); + TextField accountIdTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.phoneNr"), accountId).second; + accountIdTf.setMouseTransparent(false); + } + addLimitations(true); addCurrenciesGrid(false); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 6f468f70121..5504947bce3 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -389,9 +389,13 @@ private void setupHandlers() { showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList)); }); bisqSetup.setOsxKeyLoggerWarningHandler(() -> { - new Popup().warning(Res.get("popup.warning.osxKeyLoggerWarning")) - .closeButtonText(Res.get("shared.iUnderstand")) - .show(); + String key = "osxKeyLoggerWarning"; + if (preferences.showAgain(key)) { + new Popup().warning(Res.get("popup.warning.osxKeyLoggerWarning")) + .closeButtonText(Res.get("shared.iUnderstand")) + .dontShowAgainId(key) + .show(); + } }); corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> new Popup() diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java index 55b9e75d5de..989d063b25e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -231,7 +231,7 @@ public void activate() { if (DevEnv.isDevMode()) { UserThread.runAfter(() -> { amount.set("0.001"); - price.set("0.0001"); // for BSQ + price.set("0.008"); minAmount.set(amount.get()); onFocusOutPriceAsPercentageTextField(true, false); applyMakerFee(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java index 1f60ed26f6b..8cee9fb458a 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java @@ -98,20 +98,24 @@ public void onAdded(Offer offer) { @Override public void onRemoved(Offer offer) { - // Update state in case that that offer is used in the take offer screen, so it gets updated correctly - offer.setState(Offer.State.REMOVED); - - // clean up possible references in openOfferManager - tradeManager.onOfferRemovedFromRemoteOfferBook(offer); - // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. - Optional candidateToRemove = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) - .findAny(); - candidateToRemove.ifPresent(offerBookListItems::remove); + removeOffer(offer, tradeManager); } }); } + public void removeOffer(Offer offer, TradeManager tradeManager) { + // Update state in case that that offer is used in the take offer screen, so it gets updated correctly + offer.setState(Offer.State.REMOVED); + + // clean up possible references in openOfferManager + tradeManager.onOfferRemovedFromRemoteOfferBook(offer); + // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. + Optional candidateToRemove = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offer.getId())) + .findAny(); + candidateToRemove.ifPresent(offerBookListItems::remove); + } + public ObservableList getOfferBookListItems() { return offerBookListItems; } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index b35a9d67035..e34b3233a7c 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -19,6 +19,7 @@ import bisq.desktop.Navigation; import bisq.desktop.main.offer.OfferDataModel; +import bisq.desktop.main.offer.offerbook.OfferBook; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; @@ -81,6 +82,7 @@ */ class TakeOfferDataModel extends OfferDataModel { private final TradeManager tradeManager; + private final OfferBook offerBook; private final BsqWalletService bsqWalletService; private final User user; private final FeeService feeService; @@ -120,6 +122,7 @@ class TakeOfferDataModel extends OfferDataModel { @Inject TakeOfferDataModel(TradeManager tradeManager, + OfferBook offerBook, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, User user, FeeService feeService, @@ -134,6 +137,7 @@ class TakeOfferDataModel extends OfferDataModel { super(btcWalletService); this.tradeManager = tradeManager; + this.offerBook = offerBook; this.bsqWalletService = bsqWalletService; this.user = user; this.feeService = feeService; @@ -291,6 +295,7 @@ public void onClose() { btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// @@ -325,7 +330,17 @@ void onTakeOffer(TradeResultHandler tradeResultHandler) { offer, paymentAccount.getId(), useSavingsWallet, - tradeResultHandler, + trade -> { + // We do not wait until the offer got removed by a network remove message but remove it + // directly from the offer book. The broadcast gets now bundled and has 2 sec. delay so the + // removal from the network is a bit slower as it has been before. To avoid that the taker gets + // confused to see the same offer still in the offerbook we remove it manually. This removal has + // only local effect. Other trader might see the offer for a few seconds + // still (but cannot take it). + offerBook.removeOffer(checkNotNull(trade.getOffer()), tradeManager); + + tradeResultHandler.handleResult(trade); + }, errorMessage -> { log.warn(errorMessage); new Popup().warning(errorMessage).show(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SetXmrTxKeyWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SetXmrTxKeyWindow.java index 8862664104f..517f57be089 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SetXmrTxKeyWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SetXmrTxKeyWindow.java @@ -36,6 +36,7 @@ import javax.annotation.Nullable; import static bisq.common.app.DevEnv.isDevMode; +import static bisq.desktop.util.FormBuilder.addMultilineLabel; import static bisq.desktop.util.FormBuilder.addInputTextField; import static javafx.beans.binding.Bindings.createBooleanBinding; @@ -114,6 +115,7 @@ public String getTxKey() { } private void addContent() { + addMultilineLabel(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.note"), 0); txHashInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.txHash"), 10); txKeyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.txKey")); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 19e2ec7e1ad..6d1a288f118 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -185,7 +185,7 @@ void onSelectItem(PendingTradesListItem item) { } public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - final Trade trade = getTrade(); + Trade trade = getTrade(); checkNotNull(trade, "trade must not be null"); checkArgument(trade instanceof BuyerTrade, "Check failed: trade instanceof BuyerTrade"); ((BuyerTrade) trade).onFiatPaymentStarted(resultHandler, errorMessageHandler); @@ -194,7 +194,7 @@ public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler er public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(getTrade(), "trade must not be null"); checkArgument(getTrade() instanceof SellerTrade, "Trade must be instance of SellerTrade"); - tradeManager.onFiatPaymentReceived((SellerTrade) getTrade(), resultHandler, errorMessageHandler); + tradeManager.onUserConfirmedFiatPaymentReceived((SellerTrade) getTrade(), resultHandler, errorMessageHandler); } public void onWithdrawRequest(String toAddress, diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index dc9a93ec49d..cfc35e89681 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -26,6 +26,7 @@ import bisq.desktop.main.portfolio.pendingtrades.TradeSubView; import bisq.desktop.util.Layout; +import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeResult; @@ -390,7 +391,6 @@ protected void applyOnDisputeOpened() { } private void updateDisputeState(Trade.DisputeState disputeState) { - deactivatePaymentButtons(false); Optional ownDispute; switch (disputeState) { case NO_DISPUTE: @@ -406,7 +406,6 @@ private void updateDisputeState(Trade.DisputeState disputeState) { if (tradeStepInfo != null) tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED); }); - break; case MEDIATION_STARTED_BY_PEER: if (tradeStepInfo != null) { @@ -435,7 +434,6 @@ private void updateDisputeState(Trade.DisputeState disputeState) { updateMediationResultState(true); break; case REFUND_REQUESTED: - deactivatePaymentButtons(true); if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); } @@ -449,7 +447,6 @@ private void updateDisputeState(Trade.DisputeState disputeState) { break; case REFUND_REQUEST_STARTED_BY_PEER: - deactivatePaymentButtons(true); if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); } @@ -462,9 +459,12 @@ private void updateDisputeState(Trade.DisputeState disputeState) { }); break; case REFUND_REQUEST_CLOSED: - deactivatePaymentButtons(true); + break; + default: break; } + + updateConfirmButtonDisableState(isDisputed()); } private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) { @@ -604,7 +604,20 @@ private void openMediationResultPopup(String headLine) { acceptMediationResultPopup.show(); } - protected void deactivatePaymentButtons(boolean isDisabled) { + protected void updateConfirmButtonDisableState(boolean isDisabled) { + // By default do nothing. Only overwritten in certain trade steps + } + + protected String getCurrencyName(Trade trade) { + return CurrencyUtil.getNameByCode(getCurrencyCode(trade)); + } + + protected String getCurrencyCode(Trade trade) { + return checkNotNull(trade.getOffer()).getCurrencyCode(); + } + + protected boolean isXmrTrade() { + return getCurrencyCode(trade).equals("XMR"); } private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { @@ -639,6 +652,11 @@ private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { } } + protected boolean isDisputed() { + return trade.getDisputeState() != Trade.DisputeState.NO_DISPUTE; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // TradeDurationLimitInfo /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 562c716ebec..d6d2aeabe33 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -55,7 +55,6 @@ import bisq.desktop.util.Layout; import bisq.desktop.util.Transitions; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.network.MessageState; import bisq.core.offer.Offer; @@ -193,6 +192,8 @@ public void activate() { } }); } + + confirmButton.setDisable(isDisputed()); } @Override @@ -313,8 +314,7 @@ protected void addContent() { break; case PaymentMethod.BLOCK_CHAINS_ID: case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: - String labelTitle = Res.get("portfolio.pending.step2_buyer.sellersAddress", - CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode())); + String labelTitle = Res.get("portfolio.pending.step2_buyer.sellersAddress", getCurrencyName(trade)); gridRow = AssetsForm.addFormForBuyer(gridPane, gridRow, paymentAccountPayload, labelTitle); break; case PaymentMethod.PROMPT_PAY_ID: @@ -365,7 +365,7 @@ protected void addContent() { @Override protected String getFirstHalfOverWarnText() { return Res.get("portfolio.pending.step2_buyer.warn", - model.dataModel.getCurrencyCode(), + getCurrencyCode(trade), model.getDateForOpenDispute()); } @@ -387,6 +387,10 @@ protected void applyOnDisputeOpened() { /////////////////////////////////////////////////////////////////////////////////////////// private void onPaymentStarted() { + if (isDisputed()) { + return; + } + if (!model.dataModel.isBootstrappedOrShowPopup()) { return; } @@ -456,8 +460,7 @@ private void onPaymentStarted() { } else { showConfirmPaymentStartedPopup(); } - } else if (sellersPaymentAccountPayload instanceof AssetsAccountPayload && - checkNotNull(trade.getOffer()).getCurrencyCode().equals("XMR")) { + } else if (sellersPaymentAccountPayload instanceof AssetsAccountPayload && isXmrTrade()) { SetXmrTxKeyWindow setXmrTxKeyWindow = new SetXmrTxKeyWindow(); setXmrTxKeyWindow .actionButtonText(Res.get("portfolio.pending.step2_buyer.confirmStart.headline")) @@ -498,8 +501,7 @@ private void showConfirmPaymentStartedPopup() { if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { Popup popup = new Popup(); popup.headLine(Res.get("portfolio.pending.step2_buyer.confirmStart.headline")) - .confirmation(Res.get("portfolio.pending.step2_buyer.confirmStart.msg", - CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()))) + .confirmation(Res.get("portfolio.pending.step2_buyer.confirmStart.msg", getCurrencyName(trade))) .width(700) .actionButtonText(Res.get("portfolio.pending.step2_buyer.confirmStart.yes")) .onAction(this::confirmPaymentStarted) @@ -548,7 +550,7 @@ private void showPopup() { String amount = DisplayUtils.formatVolumeWithCode(trade.getTradeVolume()); if (paymentAccountPayload instanceof AssetsAccountPayload) { message += Res.get("portfolio.pending.step2_buyer.altcoin", - CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()), + getCurrencyName(trade), amount) + accountDetails + paymentDetailsForTradePopup + "\n\n" + @@ -631,7 +633,7 @@ private void showPopup() { } @Override - protected void deactivatePaymentButtons(boolean isDisabled) { + protected void updateConfirmButtonDisableState(boolean isDisabled) { confirmButton.setDisable(isDisabled); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java index 372a7d44801..f04305aefe9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java @@ -87,7 +87,7 @@ protected String getInfoBlockTitle() { @Override protected String getInfoText() { - return Res.get("portfolio.pending.step3_buyer.wait.info", model.dataModel.getCurrencyCode()); + return Res.get("portfolio.pending.step3_buyer.wait.info", getCurrencyCode(trade)); } private void updateMessageStateInfo() { @@ -130,7 +130,7 @@ private void updateMessageStateInfo() { @Override protected String getFirstHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? - Res.get("portfolio.pending.step3_buyer.warn.part1a", model.dataModel.getCurrencyCode()) : + Res.get("portfolio.pending.step3_buyer.warn.part1a", getCurrencyCode(trade)) : Res.get("portfolio.pending.step3_buyer.warn.part1b"); return Res.get("portfolio.pending.step3_buyer.warn.part2", substitute); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index b0d6295cfc2..29076fd47ef 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -112,7 +112,7 @@ protected void addContent() { JFXBadge autoConfBadge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT); autoConfBadge.setText(Res.get("portfolio.pending.autoConf")); - autoConfBadge.getStyleClass().add("autoconf"); + autoConfBadge.getStyleClass().add("auto-conf"); HBox hBox2 = new HBox(1, completedTradeLabel, autoConfBadge); GridPane.setMargin(hBox2, new Insets(18, -10, -12, -10)); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java index 5fb71302677..425634d2fb3 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java @@ -134,7 +134,7 @@ protected String getInfoBlockTitle() { @Override protected String getInfoText() { - return Res.get("portfolio.pending.step2_seller.waitPayment.msg", model.dataModel.getCurrencyCode()); + return Res.get("portfolio.pending.step2_seller.waitPayment.msg", getCurrencyCode(trade)); } @@ -145,7 +145,7 @@ protected String getInfoText() { @Override protected String getFirstHalfOverWarnText() { return Res.get("portfolio.pending.step2_seller.warn", - model.dataModel.getCurrencyCode(), + getCurrencyCode(trade), model.getDateForOpenDispute()); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 4bff3492dfe..e9136153b1e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -18,8 +18,10 @@ package bisq.desktop.main.portfolio.pendingtrades.steps.seller; import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.InfoTextField; import bisq.desktop.components.TextFieldWithCopyIcon; import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.indicator.TxConfidenceIndicator; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; @@ -27,8 +29,9 @@ import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.payload.AssetsAccountPayload; import bisq.core.payment.payload.BankAccountPayload; import bisq.core.payment.payload.CashDepositAccountPayload; @@ -49,6 +52,7 @@ import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.DevEnv; +import bisq.common.util.Tuple2; import bisq.common.util.Tuple4; import javafx.scene.control.Button; @@ -57,6 +61,9 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -65,10 +72,10 @@ import java.util.Optional; -import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; -import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; -import static bisq.desktop.util.FormBuilder.addTitledGroupBg; -import static bisq.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon; +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.*; +import static com.google.common.base.Preconditions.checkNotNull; public class SellerStep3View extends TradeStepView { @@ -77,8 +84,12 @@ public class SellerStep3View extends TradeStepView { private BusyAnimation busyAnimation; private Subscription tradeStatePropertySubscription; private Timer timeoutTimer; - private TextFieldWithCopyIcon assetTxProofResultField; - private final ChangeListener proofResultListener; + @Nullable + private InfoTextField assetTxProofResultField; + @Nullable + private TxConfidenceIndicator assetTxConfidenceIndicator; + @Nullable + private ChangeListener proofResultListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -87,10 +98,6 @@ public class SellerStep3View extends TradeStepView { public SellerStep3View(PendingTradesViewModel model) { super(model); - - proofResultListener = (observable, oldValue, newValue) -> { - applyAssetTxProofResult(trade.getAssetTxProofResult()); - }; } @Override @@ -151,13 +158,14 @@ public void activate() { } }); - // we listen for updates on the trade autoConfirmResult field - if (assetTxProofResultField != null) { + if (isXmrTrade()) { + proofResultListener = (observable, oldValue, newValue) -> { + applyAssetTxProofResult(trade.getAssetTxProofResult()); + }; trade.getAssetTxProofResultUpdateProperty().addListener(proofResultListener); + applyAssetTxProofResult(trade.getAssetTxProofResult()); } - - applyAssetTxProofResult(trade.getAssetTxProofResult()); } @Override @@ -175,11 +183,12 @@ public void deactivate() { timeoutTimer.stop(); } - if (assetTxProofResultField != null) { + if (isXmrTrade()) { trade.getAssetTxProofResultUpdateProperty().removeListener(proofResultListener); } } + /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @@ -203,36 +212,66 @@ protected void addContent() { String myTitle = ""; String peersTitle = ""; boolean isBlockChain = false; - String nameByCode = CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()); + String currencyName = getCurrencyName(trade); Contract contract = trade.getContract(); if (contract != null) { PaymentAccountPayload myPaymentAccountPayload = contract.getSellerPaymentAccountPayload(); PaymentAccountPayload peersPaymentAccountPayload = contract.getBuyerPaymentAccountPayload(); + + myPaymentDetails = PaymentAccountUtil.findPaymentAccount(myPaymentAccountPayload, model.getUser()) + .map(PaymentAccount::getAccountName) + .orElse(""); + if (myPaymentAccountPayload instanceof AssetsAccountPayload) { - myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); + if (myPaymentDetails.isEmpty()) { + // Not expected + myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); + } peersPaymentDetails = ((AssetsAccountPayload) peersPaymentAccountPayload).getAddress(); - myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", nameByCode); - peersTitle = Res.get("portfolio.pending.step3_seller.buyersAddress", nameByCode); + myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", currencyName); + peersTitle = Res.get("portfolio.pending.step3_seller.buyersAddress", currencyName); isBlockChain = true; } else { - myPaymentDetails = myPaymentAccountPayload.getPaymentDetails(); + if (myPaymentDetails.isEmpty()) { + // Not expected + myPaymentDetails = myPaymentAccountPayload.getPaymentDetails(); + } peersPaymentDetails = peersPaymentAccountPayload.getPaymentDetails(); myTitle = Res.get("portfolio.pending.step3_seller.yourAccount"); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAccount"); } } - if (!isBlockChain && !trade.getOffer().getPaymentMethod().equals(PaymentMethod.F2F)) { + if (!isBlockChain && !checkNotNull(trade.getOffer()).getPaymentMethod().equals(PaymentMethod.F2F)) { addTopLabelTextFieldWithCopyIcon( gridPane, gridRow, 1, Res.get("shared.reasonForPayment"), model.dataModel.getReference(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); GridPane.setRowSpan(titledGroupBg, 4); } - if (isBlockChain && trade.getOffer().getCurrencyCode().equals("XMR")) { - assetTxProofResultField = addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, - Res.get("portfolio.pending.step3_seller.autoConf.status.label"), - "", Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE).second; + if (isXmrTrade()) { + assetTxProofResultField = new InfoTextField(); + + Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("portfolio.pending.step3_seller.autoConf.status.label"), assetTxProofResultField); + VBox vBox = topLabelWithVBox.second; + + assetTxConfidenceIndicator = new TxConfidenceIndicator(); + assetTxConfidenceIndicator.setId("xmr-confidence"); + assetTxConfidenceIndicator.setProgress(0); + assetTxConfidenceIndicator.setTooltip(new Tooltip()); + assetTxProofResultField.setContentForInfoPopOver(createPopoverLabel(Res.get("setting.info.msg"))); + + HBox.setMargin(assetTxConfidenceIndicator, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + + HBox hBox = new HBox(); + HBox.setHgrow(vBox, Priority.ALWAYS); + hBox.setSpacing(10); + hBox.getChildren().addAll(vBox, assetTxConfidenceIndicator); + + GridPane.setRowIndex(hBox, gridRow); + GridPane.setColumnIndex(hBox, 1); + GridPane.setMargin(hBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + gridPane.getChildren().add(hBox); } TextFieldWithCopyIcon myPaymentDetailsTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, @@ -270,18 +309,23 @@ protected void addContent() { statusLabel = tuple.third; } + @Override + protected void updateConfirmButtonDisableState(boolean isDisabled) { + confirmButton.setDisable(isDisabled); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// - @Override protected String getInfoText() { - String currencyCode = model.dataModel.getCurrencyCode(); + String currencyName = getCurrencyName(trade); if (model.isBlockChainMethod()) { - return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.altcoin", currencyCode)); + return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.altcoin", currencyName)); } else { - return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.fiat", currencyCode)); + return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.fiat", currencyName)); } } @@ -292,7 +336,7 @@ protected String getInfoText() { @Override protected String getFirstHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? - Res.get("portfolio.pending.step3_seller.warn.part1a", model.dataModel.getCurrencyCode()) : + Res.get("portfolio.pending.step3_seller.warn.part1a", getCurrencyName(trade)) : Res.get("portfolio.pending.step3_seller.warn.part1b"); return Res.get("portfolio.pending.step3_seller.warn.part2", substitute); @@ -317,13 +361,17 @@ protected void applyOnDisputeOpened() { /////////////////////////////////////////////////////////////////////////////////////////// private void onPaymentReceived() { + if (isDisputed()) { + return; + } + // The confirmPaymentReceived call will trigger the trade protocol to do the payout tx. We want to be sure that we // are well connected to the Bitcoin network before triggering the broadcast. if (model.dataModel.isReadyForTxBroadcast()) { String key = "confirmPaymentReceived"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); - String message = Res.get("portfolio.pending.step3_seller.onPaymentReceived.part1", CurrencyUtil.getNameByCode(model.dataModel.getCurrencyCode())); + String message = Res.get("portfolio.pending.step3_seller.onPaymentReceived.part1", getCurrencyName(trade)); if (!(paymentAccountPayload instanceof AssetsAccountPayload)) { if (!(paymentAccountPayload instanceof WesternUnionAccountPayload) && !(paymentAccountPayload instanceof HalCashAccountPayload) && @@ -359,12 +407,12 @@ private void showPopup() { String key = "confirmPayment" + trade.getId(); String message = ""; String tradeVolumeWithCode = DisplayUtils.formatVolumeWithCode(trade.getTradeVolume()); - String currencyName = CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()); + String currencyName = getCurrencyName(trade); String part1 = Res.get("portfolio.pending.step3_seller.part", currencyName); String id = trade.getShortId(); if (paymentAccountPayload instanceof AssetsAccountPayload) { String address = ((AssetsAccountPayload) paymentAccountPayload).getAddress(); - String explorerOrWalletString = trade.getOffer().getCurrencyCode().equals("XMR") ? + String explorerOrWalletString = isXmrTrade() ? Res.get("portfolio.pending.step3_seller.altcoin.wallet", currencyName) : Res.get("portfolio.pending.step3_seller.altcoin.explorer", currencyName); message = Res.get("portfolio.pending.step3_seller.altcoin", part1, explorerOrWalletString, address, tradeVolumeWithCode, currencyName); @@ -443,14 +491,47 @@ else if (paymentAccountPayload instanceof SepaInstantAccountPayload) } } - private void applyAssetTxProofResult(AssetTxProofResult result) { + private void applyAssetTxProofResult(@Nullable AssetTxProofResult result) { + checkNotNull(assetTxProofResultField); + checkNotNull(assetTxConfidenceIndicator); + String txt = GUIUtil.getProofResultAsString(result); assetTxProofResultField.setText(txt); - assetTxProofResultField.setTooltip(new Tooltip(txt)); + + if (result == null) { + assetTxConfidenceIndicator.setProgress(0); + return; + } + + switch (result) { + case PENDING: + case COMPLETED: + if (result.getNumRequiredConfirmations() > 0) { + int numRequiredConfirmations = result.getNumRequiredConfirmations(); + int numConfirmations = result.getNumConfirmations(); + if (numConfirmations == 0) { + assetTxConfidenceIndicator.setProgress(-1); + } else { + double progress = Math.min(1, (double) numConfirmations / (double) numRequiredConfirmations); + assetTxConfidenceIndicator.setProgress(progress); + assetTxConfidenceIndicator.getTooltip().setText( + Res.get("portfolio.pending.autoConf.blocks", + numConfirmations, numRequiredConfirmations)); + } + } + break; + default: + // Set invisible by default + assetTxConfidenceIndicator.setProgress(0); + break; + } } - @Override - protected void deactivatePaymentButtons(boolean isDisabled) { - confirmButton.setDisable(isDisabled); + private Label createPopoverLabel(String text) { + Label label = new Label(text); + label.setPrefWidth(600); + label.setWrapText(true); + label.setPadding(new Insets(10)); + return label; } } diff --git a/desktop/src/main/java/bisq/desktop/main/settings/about/AboutView.java b/desktop/src/main/java/bisq/desktop/main/settings/about/AboutView.java index 8b0113fd379..5dcedaf1528 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/about/AboutView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/about/AboutView.java @@ -93,7 +93,7 @@ public void initialize() { Version.LOCAL_DB_VERSION, Version.TRADE_PROTOCOL_VERSION)); - addTitledGroupBg(root, ++gridRow, 20, Res.get("setting.about.shortcuts"), Layout.GROUP_DISTANCE); + addTitledGroupBg(root, ++gridRow, 18, Res.get("setting.about.shortcuts"), Layout.GROUP_DISTANCE); // basics addCompactTopLabelTextField(root, gridRow, Res.get("setting.about.shortcuts.menuNav"), @@ -122,10 +122,6 @@ public void initialize() { addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.openEmergencyBsqWalletTool"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "b")); - addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.showDisputeStatistics"), - Res.get("setting.about.shortcuts.showDisputeStatistics.value", - Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "l"))); - addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.showTorLogs"), Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "t")); @@ -149,10 +145,6 @@ public void initialize() { Res.get("setting.about.shortcuts.registerMediator.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "d"))); - addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.reOpenDispute"), - Res.get("setting.about.shortcuts.reOpenDispute.value", - Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "u"))); - addCompactTopLabelTextField(root, ++gridRow, Res.get("setting.about.shortcuts.openSignPaymentAccountsWindow"), Res.get("setting.about.shortcuts.openSignPaymentAccountsWindow.value", Res.get("setting.about.shortcuts.ctrlOrAltOrCmd", "s"))); diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 60ab77a3418..f9c803be1e9 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -95,6 +95,7 @@ import java.io.File; import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -661,25 +662,52 @@ private void initializeAutoConfirmOptions() { autoConfirmXmrToggle = addSlideToggleButton(autoConfirmGridPane, localRowIndex, Res.get("setting.preferences.autoConfirmEnabled"), Layout.FIRST_ROW_DISTANCE); autoConfRequiredConfirmationsTf = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("setting.preferences.autoConfirmRequiredConfirmations")); - autoConfRequiredConfirmationsTf.setValidator(new IntegerValidator(0, DevEnv.isDevMode() ? 100000000 : 1000)); + autoConfRequiredConfirmationsTf.setValidator(new IntegerValidator(1, DevEnv.isDevMode() ? 100000000 : 1000)); autoConfTradeLimitTf = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("setting.preferences.autoConfirmMaxTradeSize")); autoConfTradeLimitTf.setValidator(new BtcValidator(formatter)); autoConfServiceAddressTf = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("setting.preferences.autoConfirmServiceAddresses")); - autoConfServiceAddressTf.setValidator(GUIUtil.addressRegexValidator()); - autoConfServiceAddressTf.setErrorMessage(Res.get("validation.invalidAddressList")); GridPane.setHgrow(autoConfServiceAddressTf, Priority.ALWAYS); displayCurrenciesGridRowIndex += 4; autoConfServiceAddressListener = (observable, oldValue, newValue) -> { - if (!newValue.equals(oldValue) && autoConfServiceAddressTf.getValidator().validate(newValue).isValid) { - List serviceAddresses = Arrays.asList(StringUtils.deleteWhitespace(newValue).split(",")); + if (!newValue.equals(oldValue)) { + + RegexValidator onionRegex = GUIUtil.onionAddressRegexValidator(); + RegexValidator localhostRegex = GUIUtil.localhostAddressRegexValidator(); + RegexValidator localnetRegex = GUIUtil.localnetAddressRegexValidator(); + + List serviceAddressesRaw = Arrays.asList(StringUtils.deleteWhitespace(newValue).split(",")); + // revert to default service providers when user empties the list - if (serviceAddresses.size() == 1 && serviceAddresses.get(0).isEmpty()) { - serviceAddresses = Preferences.getDefaultXmrProofProviders(); + if (serviceAddressesRaw.size() == 1 && serviceAddressesRaw.get(0).isEmpty()) { + serviceAddressesRaw = preferences.getDefaultXmrTxProofServices(); } - preferences.setAutoConfServiceAddresses("XMR", serviceAddresses); + + // we must always communicate with XMR explorer API securely + // if *.onion hostname, we use Tor normally + // if localhost, LAN address, or *.local FQDN we use HTTP without Tor + // otherwise we enforce https:// for any clearnet FQDN hostname + List serviceAddressesParsed = new ArrayList(); + serviceAddressesRaw.forEach((addr) -> { + addr = addr.replaceAll("http://", "").replaceAll("https://", ""); + if (onionRegex.validate(addr).isValid) { + log.info("Using Tor for onion hostname: {}", addr); + serviceAddressesParsed.add(addr); + } else if (localhostRegex.validate(addr).isValid) { + log.info("Using HTTP without Tor for Loopback address: {}", addr); + serviceAddressesParsed.add("http://" + addr); + } else if (localnetRegex.validate(addr).isValid) { + log.info("Using HTTP without Tor for LAN address: {}", addr); + serviceAddressesParsed.add("http://" + addr); + } else { + log.info("Using HTTPS with Tor for Clearnet address: {}", addr); + serviceAddressesParsed.add("https://" + addr); + } + }); + + preferences.setAutoConfServiceAddresses("XMR", serviceAddressesParsed); } }; diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 6bf28f446cb..fcba3c3b652 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -60,18 +60,19 @@ import com.google.common.collect.Lists; -import javafx.scene.Scene; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; import javafx.geometry.Insets; @@ -82,8 +83,6 @@ import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; -import javafx.event.EventHandler; - import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -108,6 +107,8 @@ import javax.annotation.Nullable; +import static bisq.desktop.util.FormBuilder.getIconForLabel; + public abstract class DisputeView extends ActivatableView { protected final DisputeManager> disputeManager; @@ -122,7 +123,7 @@ public abstract class DisputeView extends ActivatableView { private final AccountAgeWitnessService accountAgeWitnessService; private final boolean useDevPrivilegeKeys; - private TableView tableView; + protected TableView tableView; private SortedList sortedList; @Getter @@ -132,16 +133,15 @@ public abstract class DisputeView extends ActivatableView { private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; - private EventHandler keyEventEventHandler; - private Scene scene; protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - private HBox filterBox; protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; private Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases + protected Label alertIconLabel; + protected TableColumn stateColumn; /////////////////////////////////////////////////////////////////////////////////////////// @@ -180,6 +180,14 @@ public void initialize() { filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); HBox.setHgrow(filterTextField, Priority.NEVER); + alertIconLabel = new Label(); + Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "2em", alertIconLabel); + icon.getStyleClass().add("alert-icon"); + HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10)); + alertIconLabel.setMouseTransparent(false); + alertIconLabel.setVisible(false); + alertIconLabel.setManaged(false); + reOpenButton = new AutoTooltipButton(Res.get("support.reOpenButton.label")); reOpenButton.setDisable(true); reOpenButton.setVisible(false); @@ -217,9 +225,16 @@ public void initialize() { Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); - filterBox = new HBox(); + HBox filterBox = new HBox(); filterBox.setSpacing(5); - filterBox.getChildren().addAll(label, filterTextField, spacer, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton); + filterBox.getChildren().addAll(label, + filterTextField, + alertIconLabel, + spacer, + reOpenButton, + sendPrivateNotificationButton, + reportButton, + fullReportButton); VBox.setVgrow(filterBox, Priority.NEVER); tableView = new TableView<>(); @@ -232,8 +247,6 @@ public void initialize() { selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); - keyEventEventHandler = this::handleKeyPressed; - chatView = new ChatView(disputeManager, formatter); chatView.initialize(); } @@ -263,9 +276,6 @@ else if (sortedList.size() > 0) chatView.scrollToBottom(); } - scene = root.getScene(); - if (scene != null) - scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); // If doPrint=true we print out a html page which opens tabs with all deposit txs // (firefox needs about:config change to allow > 20 tabs) @@ -324,9 +334,6 @@ protected void deactivate() { selectedDisputeSubscription.unsubscribe(); removeListenersOnSelectDispute(); - if (scene != null) - scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - if (chatView != null) chatView.deactivate(); } @@ -398,9 +405,6 @@ protected void onCloseDispute(Dispute dispute) { } } - protected void handleKeyPressed(KeyEvent event) { - } - protected void reOpenDispute() { if (selectedDispute != null) { selectedDispute.setIsClosed(false); @@ -712,7 +716,7 @@ private void showFullReport() { // Table /////////////////////////////////////////////////////////////////////////////////////////// - private void setupTable() { + protected void setupTable() { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); placeholder.setWrapText(true); @@ -743,7 +747,7 @@ private void setupTable() { TableColumn roleColumn = getRoleColumn(); tableView.getColumns().add(roleColumn); - TableColumn stateColumn = getStateColumn(); + stateColumn = getStateColumn(); tableView.getColumns().add(stateColumn); tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); @@ -1099,7 +1103,6 @@ public void updateItem(final Dispute item, boolean empty) { }); return column; } - } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java index 582c461b087..13e52608678 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java @@ -18,6 +18,8 @@ package bisq.desktop.main.support.dispute.agent; import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; @@ -30,14 +32,35 @@ import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.agent.MultipleHolderNameDetection; import bisq.core.trade.TradeManager; +import bisq.core.user.DontShowAgainLookup; import bisq.core.util.coin.CoinFormatter; import bisq.common.crypto.KeyRing; +import bisq.common.util.Utilities; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.text.Text; + +import javafx.geometry.Insets; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import java.util.List; + +import static bisq.desktop.util.FormBuilder.getIconForLabel; + +public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener { -public abstract class DisputeAgentView extends DisputeView { + private final MultipleHolderNameDetection multipleHolderNameDetection; public DisputeAgentView(DisputeManager> disputeManager, KeyRing keyRing, @@ -59,8 +82,15 @@ public DisputeAgentView(DisputeManager { @@ -101,6 +165,131 @@ protected void handleOnSelectDispute(Dispute dispute) { DisputeSession chatSession = getConcreteDisputeChatSession(dispute); chatView.display(chatSession, closeDisputeButton, root.widthProperty()); } + + @Override + protected void setupTable() { + super.setupTable(); + + stateColumn.getStyleClass().remove("last-column"); + tableView.getColumns().add(getAlertColumn()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void suspiciousDisputeDetected() { + alertIconLabel.setVisible(true); + alertIconLabel.setManaged(true); + alertIconLabel.setTooltip(new Tooltip("You have suspicious disputes where the same trader used different " + + "account holder names.\nClick for more information.")); + // Text below is for arbitrators only so no need to translate it + alertIconLabel.setOnMouseClicked(e -> { + String reportForAllDisputes = multipleHolderNameDetection.getReportForAllDisputes(); + new Popup() + .width(1100) + .warning(getReportMessage(reportForAllDisputes, "traders")) + .actionButtonText(Res.get("shared.copyToClipboard")) + .onAction(() -> Utilities.copyToClipboard(reportForAllDisputes)) + .show(); + }); + } + + + private TableColumn getAlertColumn() { + TableColumn column = new AutoTooltipTableColumn<>("Alert") { + { + setMinWidth(50); + } + }; + column.getStyleClass().add("last-column"); + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + c -> new TableCell<>() { + Label alertIconLabel; + + @Override + public void updateItem(Dispute dispute, boolean empty) { + if (dispute != null && !empty) { + if (!showAlertAtDispute(dispute)) { + setGraphic(null); + if (alertIconLabel != null) { + alertIconLabel.setOnMouseClicked(null); + } + return; + } + + if (alertIconLabel != null) { + alertIconLabel.setOnMouseClicked(null); + } + + alertIconLabel = new Label(); + Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "1.5em", alertIconLabel); + icon.getStyleClass().add("alert-icon"); + HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10)); + alertIconLabel.setMouseTransparent(false); + setGraphic(alertIconLabel); + + alertIconLabel.setOnMouseClicked(e -> { + List realNameAccountInfoList = multipleHolderNameDetection.getDisputesForTrader(dispute); + String reportForDisputeOfTrader = multipleHolderNameDetection.getReportForDisputeOfTrader(realNameAccountInfoList); + String key = MultipleHolderNameDetection.getAckKey(dispute); + new Popup() + .width(1100) + .warning(getReportMessage(reportForDisputeOfTrader, "this trader")) + .actionButtonText(Res.get("shared.copyToClipboard")) + .onAction(() -> { + Utilities.copyToClipboard(reportForDisputeOfTrader); + if (!DontShowAgainLookup.showAgain(key)) { + setGraphic(null); + } + }) + .dontShowAgainId(key) + .dontShowAgainText("Is not suspicious") + .onClose(() -> { + if (!DontShowAgainLookup.showAgain(key)) { + setGraphic(null); + } + }) + .show(); + }); + } else { + setGraphic(null); + if (alertIconLabel != null) { + alertIconLabel.setOnMouseClicked(null); + } + } + } + }); + + column.setComparator((o1, o2) -> Boolean.compare(showAlertAtDispute(o1), showAlertAtDispute(o2))); + column.setSortable(true); + return column; + } + + private boolean showAlertAtDispute(Dispute dispute) { + return DontShowAgainLookup.showAgain(MultipleHolderNameDetection.getAckKey(dispute)) && + !multipleHolderNameDetection.getDisputesForTrader(dispute).isEmpty(); + } + + private String getReportMessage(String report, String subString) { + return "You have dispute cases where " + subString + " used different account holder names.\n\n" + + "This might be not critical in case of small variations of the same name " + + "(e.g. first name and last name are swapped), " + + "but if the name is completely different you should request information from the trader why they " + + "used a different name and request proof that the person with the real name is aware " + + "of the trade. " + + "It can be that the trader uses the account of their wife/husband, but it also could " + + "be a case of a stolen bank account or money laundering.\n\n" + + "Please check below the list of the names which have been detected. " + + "Search with the trade ID for the dispute case or check out the alert icon at each dispute in " + + "the list (you might need to remove the 'open' filter) and evaluate " + + "if it might be a fraudulent account (buyer role is more likely to be fraudulent). " + + "If you find suspicious disputes, please notify the developers and provide the contract json data " + + "to them so they can ban those traders.\n\n" + + Utilities.toTruncatedString(report, 700, false); + } } diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 6ab66cce4de..0bc812fdded 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -139,6 +139,9 @@ /* dao chart colors */ -bs-chart-dao-line1: -bs-color-green-5; -bs-chart-dao-line2: -bs-color-blue-2; + + /* Monero orange color code */ + -xmr-orange: #f26822; } /* table view */ diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index 165d4e9d082..910f7f4233d 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -106,6 +106,9 @@ /* dao chart colors */ -bs-chart-dao-line1: -bs-color-green-3; -bs-chart-dao-line2: -bs-color-blue-5; + + /* Monero orange color code */ + -xmr-orange: #f26822; } .warning-box { diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 3fb3ad6b27f..5fabd2c94a9 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -1161,6 +1161,127 @@ public static RegexValidator addressRegexValidator() { return regexValidator; } + // checks if valid tor onion hostname with optional port at the end + public static RegexValidator onionAddressRegexValidator() { + RegexValidator regexValidator = new RegexValidator(); + String portRegexPattern = "(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"; + String onionV2RegexPattern = String.format("[a-zA-Z2-7]{16}\\.onion(?:\\:%1$s)?", portRegexPattern); + String onionV3RegexPattern = String.format("[a-zA-Z2-7]{56}\\.onion(?:\\:%1$s)?", portRegexPattern); + regexValidator.setPattern(String.format("^(?:(?:(?:%1$s)|(?:%2$s)),\\s*)*(?:(?:%1$s)|(?:%2$s))*$", + onionV2RegexPattern, onionV3RegexPattern)); + return regexValidator; + } + + // checks if localhost address, with optional port at the end + public static RegexValidator localhostAddressRegexValidator() { + RegexValidator regexValidator = new RegexValidator(); + + // match 0 ~ 65535 + String portRegexPattern = "(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"; + + // match 127/8 (127.0.0.0 ~ 127.255.255.255) + String localhostIpv4RegexPattern = String.format( + "(?:127\\.)" + + "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){2}" + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "(?:\\:%1$s)?", + portRegexPattern); + + // match ::/64 with optional port at the end, i.e. ::1 or [::1]:8081 + String localhostIpv6RegexPattern = "(:((:[0-9a-fA-F]{1,4}){1,4}|:)|)"; + localhostIpv6RegexPattern = String.format("(?:%1$s)|(?:\\[%1$s\\]\\:%2$s)", localhostIpv6RegexPattern, portRegexPattern); + + // match *.local + String localhostFqdnRegexPattern = String.format("(localhost(?:\\:%1$s)?)", portRegexPattern); + + regexValidator.setPattern(String.format("^(?:(?:(?:%1$s)|(?:%2$s)|(?:%3$s)),\\s*)*(?:(?:%1$s)|(?:%2$s)|(?:%3$s))*$", + localhostIpv4RegexPattern, localhostIpv6RegexPattern, localhostFqdnRegexPattern)); + + return regexValidator; + } + + // checks if local area network address, with optional port at the end + public static RegexValidator localnetAddressRegexValidator() { + RegexValidator regexValidator = new RegexValidator(); + + // match 0 ~ 65535 + String portRegexPattern = "(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])"; + + // match 10/8 (10.0.0.0 ~ 10.255.255.255) + String localnetIpv4RegexPatternA = String.format( + "(?:10\\.)" + + "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){2}" + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "(?:\\:%1$s)?", + portRegexPattern); + + // match 172.16/12 (172.16.0.0 ~ 172.31.255.255) + String localnetIpv4RegexPatternB = String.format( + "(?:172\\.)" + + "(?:(?:1[6-9]|2[0-9]|[3][0-1])\\.)" + + "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.)" + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "(?:\\:%1$s)?", + portRegexPattern); + + // match 192.168/16 (192.168.0.0 ~ 192.168.255.255) + String localnetIpv4RegexPatternC = String.format( + "(?:192\\.)" + + "(?:168\\.)" + + "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.)" + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "(?:\\:%1$s)?", + portRegexPattern); + + // match 169.254/15 (169.254.0.0 ~ 169.255.255.255) + String autolocalIpv4RegexPattern = String.format( + "(?:169\\.)" + + "(?:(?:254|255)\\.)" + + "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.)" + + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "(?:\\:%1$s)?", + portRegexPattern); + + // match fc00::/7 (fc00:: ~ fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) + String localnetIpv6RegexPattern = "(" + + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|" + // fd00:2:3:4:5:6:7:8 + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,7}:|" + // fd00:: fd00:2:3:4:5:6:7:: + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,6}:[0-9a-fA-F]{1,4}|" + // fd00::8 fd00:2:3:4:5:6::8 fd00:2:3:4:5:6::8 + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,5}(:[0-9a-fA-F]{1,4}){1,1}|" + // fd00::7:8 fd00:2:3:4:5::7:8 fd00:2:3:4:5::8 + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,4}(:[0-9a-fA-F]{1,4}){1,2}|" + // fd00::7:8 fd00:2:3:4:5::7:8 fd00:2:3:4:5::8 + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,3}(:[0-9a-fA-F]{1,4}){1,3}|" + // fd00::6:7:8 fd00:2:3:4::6:7:8 fd00:2:3:4::8 + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,2}(:[0-9a-fA-F]{1,4}){1,4}|" + // fd00::5:6:7:8 fd00:2:3::5:6:7:8 fd00:2:3::8 + "([fF][cCdD][0-9a-fA-F]{2}:)([0-9a-fA-F]{1,4}:){0,1}(:[0-9a-fA-F]{1,4}){1,5}|" + // fd00::4:5:6:7:8 fd00:2::4:5:6:7:8 fd00:2::8 + "([fF][cCdD][0-9a-fA-F]{2}:)(:[0-9a-fA-F]{1,4}){1,6}" + // fd00::3:4:5:6:7:8 fd00::3:4:5:6:7:8 fd00::8 + ")"; + + // match fe80::/10 (fe80:: ~ febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff) + String autolocalIpv6RegexPattern = "(" + + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|" + // fe80:2:3:4:5:6:7:8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,7}:|" + // fe80:: fe80:2:3:4:5:6:7:: + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,6}:[0-9a-fA-F]{1,4}|" + // fe80::8 fe80:2:3:4:5:6::8 fe80:2:3:4:5:6::8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,5}(:[0-9a-fA-F]{1,4}){1,1}|" + // fe80::7:8 fe80:2:3:4:5::7:8 fe80:2:3:4:5::8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,4}(:[0-9a-fA-F]{1,4}){1,2}|" + // fe80::7:8 fe80:2:3:4:5::7:8 fe80:2:3:4:5::8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,3}(:[0-9a-fA-F]{1,4}){1,3}|" + // fe80::6:7:8 fe80:2:3:4::6:7:8 fe80:2:3:4::8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,2}(:[0-9a-fA-F]{1,4}){1,4}|" + // fe80::5:6:7:8 fe80:2:3::5:6:7:8 fe80:2:3::8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)([0-9a-fA-F]{1,4}:){0,1}(:[0-9a-fA-F]{1,4}){1,5}|" + // fe80::4:5:6:7:8 fe80:2::4:5:6:7:8 fe80:2::8 + "([fF][eE][8-9a-bA-B][0-9a-fA-F]:)(:[0-9a-fA-F]{1,4}){1,6}" + // fe80::3:4:5:6:7:8 fe80::3:4:5:6:7:8 fe80::8 + ")"; + + // allow for brackets with optional port at the end + localnetIpv6RegexPattern = String.format("(?:%1$s)|(?:\\[%1$s\\]\\:%2$s)", localnetIpv6RegexPattern, portRegexPattern); + + // allow for brackets with optional port at the end + autolocalIpv6RegexPattern = String.format("(?:%1$s)|(?:\\[%1$s\\]\\:%2$s)", autolocalIpv6RegexPattern, portRegexPattern); + + // match *.local + String localFqdnRegexPattern = String.format("(((?!-)[a-zA-Z0-9-]{1,63}(? { try { log.info("\n################################################################\n" + - "Tor hidden service published after {} ms. Socked={}\n" + + "Tor hidden service published after {} ms. Socket={}\n" + "################################################################", (new Date().getTime() - ts2), socket); //takes usually 30-40 sec new Thread() { diff --git a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java index 2283547a1f7..9dce1c55351 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java @@ -29,6 +29,7 @@ import bisq.network.p2p.peers.getdata.messages.GetDataResponse; import bisq.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; import bisq.network.p2p.peers.getdata.messages.PreliminaryGetDataRequest; +import bisq.network.p2p.seed.SeedNodeRepository; import bisq.network.p2p.storage.messages.AddDataMessage; import bisq.network.p2p.storage.messages.AddOncePayload; import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; @@ -54,6 +55,7 @@ import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.Capabilities; +import bisq.common.app.Version; import bisq.common.crypto.CryptoException; import bisq.common.crypto.Hash; import bisq.common.crypto.Sig; @@ -80,7 +82,10 @@ import java.time.Clock; +import java.nio.charset.StandardCharsets; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -115,6 +120,8 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers @VisibleForTesting public static final int CHECK_TTL_INTERVAL_SEC = 60; + private final SeedNodeRepository seedNodeRepository; + private final NetworkNode networkNode; private boolean initialRequestApplied = false; @@ -152,15 +159,17 @@ public P2PDataStorage(NetworkNode networkNode, ProtectedDataStoreService protectedDataStoreService, ResourceDataStoreService resourceDataStoreService, Storage sequenceNumberMapStorage, + SeedNodeRepository seedNodeRepository, Clock clock, @Named("MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE") int maxSequenceNumberBeforePurge) { this.broadcaster = broadcaster; this.appendOnlyDataStoreService = appendOnlyDataStoreService; this.protectedDataStoreService = protectedDataStoreService; this.resourceDataStoreService = resourceDataStoreService; + this.seedNodeRepository = seedNodeRepository; this.clock = clock; this.maxSequenceNumberMapSizeBeforePurge = maxSequenceNumberBeforePurge; - + this.networkNode = networkNode; networkNode.addMessageListener(this); networkNode.addConnectionListener(this); @@ -205,28 +214,80 @@ public GetUpdatedDataRequest buildGetUpdatedDataRequest(NodeAddress senderNodeAd return new GetUpdatedDataRequest(senderNodeAddress, nonce, this.getKnownPayloadHashes()); } + /** + * Create the special key.

+ * + *
    + *
  • A "key" is a 20 byte cryptographic hash code to identify a single p2p network message
  • + *
  • A "special key" is the label to a bundle of p2p network messages, each having standard keys
  • + *
+ * + * For example: "1.3.4" encoded into a 20 byte array. + * + * @return the special key + */ + private byte[] getSpecialKey() { + byte[] result = new byte[20]; + Arrays.fill(result, (byte) 0); + System.arraycopy(Version.VERSION.getBytes(StandardCharsets.UTF_8), 0, result, 0, Version.VERSION.length()); + return result; + } + + /** + * See if the request contains a "special key". A special key is a label of a bundle of messages. + *
    + *
  • A "key" is a 20 byte cryptographic hash code to identify a single p2p network message
  • + *
  • A "special key" is the label to a bundle of p2p network messages, each having standard keys
  • + *
+ * + * @param knownPayloadHashes + * @return the "special key" or null if no special key has been found + */ + private String findSpecialKey(Set knownPayloadHashes) { + return knownPayloadHashes.stream() + .map(byteArray -> new String(byteArray.bytes, StandardCharsets.UTF_8).trim()) + .filter(s -> s.matches("^[0-9]\\.[0-9]\\.[0-9]$")) + .findFirst() + .orElse(null); + } + /** * Returns the set of known payload hashes. This is used in the GetData path to request missing data from peer nodes */ private Set getKnownPayloadHashes() { + // We collect the keys of the PersistableNetworkPayload items so we exclude them in our request. // PersistedStoragePayload items don't get removed, so we don't have an issue with the case that // an object gets removed in between PreliminaryGetDataRequest and the GetUpdatedDataRequest and we would // miss that event if we do not load the full set or use some delta handling. - Set excludedKeys = this.appendOnlyDataStoreService.getMap().keySet().stream() - .map(e -> e.bytes) - .collect(Collectors.toSet()); + Set excludedKeys; + boolean weAreASeedNode = seedNodeRepository != null && seedNodeRepository.isSeedNode(networkNode.getNodeAddress()); + if (weAreASeedNode) { + excludedKeys = getKeySetInBytes(this.appendOnlyDataStoreService.getMap()); + } else { + excludedKeys = getKeySetInBytes(this.appendOnlyDataStoreService.getMap("since " + Version.VERSION)); + } - Set excludedKeysFromPersistedEntryMap = this.map.keySet() - .stream() - .map(e -> e.bytes) - .collect(Collectors.toSet()); + Set excludedKeysFromPersistedEntryMap = getKeySetInBytes(this.map); excludedKeys.addAll(excludedKeysFromPersistedEntryMap); + excludedKeys.add(getSpecialKey()); return excludedKeys; } + /** + * Helper for extracting hash bytes from a map of objects. + * + * @param input a map of objects + * @return a list of hash bytes of the objects in the input map + */ + private Set getKeySetInBytes(Map input) { + return input.keySet().stream() + .map(e -> e.bytes) + .collect(Collectors.toSet()); + } + /** * Generic function that can be used to filter a Map * by a given set of keys and peer capabilities. @@ -268,9 +329,19 @@ public GetDataResponse buildGetDataResponse( Set excludedKeysAsByteArray = P2PDataStorage.ByteArray.convertBytesSetToByteArraySet(getDataRequest.getExcludedKeys()); + // In case we get a "new" data request, ie. with a "special key" like "1.3.4", we + // pre-filter the data. If there is no "special key", we use all data. + Map prefilteredData; + String specialKey = findSpecialKey(excludedKeysAsByteArray); + if (specialKey == null) { + prefilteredData = this.appendOnlyDataStoreService.getMap(); + } else { + prefilteredData = this.appendOnlyDataStoreService.getMap("since " + specialKey); + } + Set filteredPersistableNetworkPayloads = filterKnownHashes( - this.appendOnlyDataStoreService.getMap(), + prefilteredData, Function.identity(), excludedKeysAsByteArray, peerCapabilities, diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java index c7ea20c9438..89f50931a86 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/AppendOnlyDataStoreService.java @@ -55,6 +55,22 @@ public void readFromResources(String postFix) { services.forEach(service -> service.readFromResources(postFix)); } + /** + * Same as {@link AppendOnlyDataStoreService#getMap()}, but takes a filter string. + * Currently, a string of format "since " + a version string is supported. Eg. "since 1.3.5". + * + * The filter string is kept generic so that we may not need to change the API again + * once other filters become necessary in the future. + * + * @param filter + * @return + */ + public Map getMap(String filter) { + return services.stream() + .flatMap(service -> service.getMap(filter).entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + public Map getMap() { return services.stream() .flatMap(service -> service.getMap().entrySet().stream()) diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java index 67088799c4a..364c17e0a9e 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java @@ -54,14 +54,25 @@ public MapStoreService(File storageDir, Storage storage) { public abstract Map getMap(); + /** + * reroute the filtered query to a non-filtered by default. + *

+ * only stores that are shipped with a release use it anyways and they have to override + * the method to take advantage of the feature. + *

+ */ + public Map getMap(String filter) { + return getMap(); + } + public abstract boolean canHandle(R payload); - void put(P2PDataStorage.ByteArray hash, R payload) { + protected void put(P2PDataStorage.ByteArray hash, R payload) { getMap().put(hash, payload); persist(); } - R putIfAbsent(P2PDataStorage.ByteArray hash, R payload) { + protected R putIfAbsent(P2PDataStorage.ByteArray hash, R payload) { R previous = getMap().putIfAbsent(hash, payload); persist(); return previous; diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/SplitStore.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/SplitStore.java new file mode 100644 index 00000000000..dbf0acb9082 --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/SplitStore.java @@ -0,0 +1,19 @@ +package bisq.network.p2p.storage.persistence; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import bisq.common.proto.persistable.ThreadedPersistableEnvelope; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.Getter; + +/** + * Goes with {@link SplitStoreService}. + */ +public abstract class SplitStore implements ThreadedPersistableEnvelope { + @Getter + protected Map map = new ConcurrentHashMap<>(); +} diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/SplitStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/SplitStoreService.java new file mode 100644 index 00000000000..ede4c7e9be4 --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/SplitStoreService.java @@ -0,0 +1,202 @@ +package bisq.network.p2p.storage.persistence; + +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import bisq.common.app.Version; +import bisq.common.storage.FileUtil; +import bisq.common.storage.ResourceNotFoundException; +import bisq.common.storage.Storage; + +import java.io.File; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +/** + * Has business logic to operate data stores which are spread across multiple files.

+ * + *

Use Case

+ * Startup requires to send all object keys to the network in order to get the new data. + * However, data stores got quite big over time and will grow even faster in the future. + * With multi-file data stores, we can query for new objects since the last snapshot (ie. + * the last release) and subsequently send "I am Bisq vx.y.z and I have these new objects" + * to the network and the network can respond accordingly.



+ * + *

Features

+ * In order to only get a specific part of all the objects available in the complete data + * store, the original single-file data store had to be split up and this class and + * {@link SplitStore} are there to handle the business logic needed for + *
    + *
  • migrating to the new and shiny multi-file data store
  • + *
  • shoveling around data in case Bisq gets updated to a new version
  • + *
  • takes care of setting up a fresh Bisq install
  • + *
  • makes sure that historical data cannot be altered easily
  • + *
  • adding data to the store will only amend the live store
  • + *
  • takes care of keeping the legacy API functional
  • + *
  • adds the feature of filtering object queries by Bisq release
  • + *
+ *

+ * + *

Further reading

+ */ +@Slf4j +public abstract class SplitStoreService extends MapStoreService { + protected HashMap history; + + public SplitStoreService(File storageDir, Storage storage) { + super(storageDir, storage); + } + + @Override + protected void put(P2PDataStorage.ByteArray hash, PersistableNetworkPayload payload) { + // make sure we do not add data that we already have (in a bin of historical data) + if (getMap().containsKey(hash)) + return; + + store.getMap().put(hash, payload); + persist(); + } + + @Override + protected PersistableNetworkPayload putIfAbsent(P2PDataStorage.ByteArray hash, + PersistableNetworkPayload payload) { + // make sure we do not add data that we already have (in a bin of historical data) + if (getMap().containsKey(hash)) + return null; + + PersistableNetworkPayload previous = store.getMap().put(hash, payload); + persist(); + return previous; + } + + + @Override + public Map getMap() { + HashMap result = new HashMap<>(); + result.putAll(store.getMap()); + history.forEach((s, store) -> result.putAll(store.getMap())); + + return result; + } + + @Override + public Map getMap(String filter) { + HashMap result = new HashMap<>(); + result.putAll(store.getMap()); + + // TODO do a proper language, possibly classes + if (filter.startsWith("since ")) { + String finalFilter = filter.replace("since ", ""); + if (!finalFilter.equals(Version.VERSION)) { + history.entrySet().stream() + .filter(entry -> parseSpecialKey(entry.getKey()) > parseSpecialKey(finalFilter)) + .forEach(entry -> result.putAll(entry.getValue().getMap())); + } + } + + return result; + } + + private int parseSpecialKey(String specialKey) { + return Integer.parseInt(specialKey.replace(".", "")); + } + + /** + * For the {@link SplitStoreService}s, we check if we already have all the historical data stores in our working + * directory. If we have, we can proceed loading the stores. If we do not, we have to transfer the fresh data stores + * from resources. + * + * @param postFix + */ + @Override + protected void readFromResources(String postFix) { + // initialize here in case this method gets called twice + history = new HashMap<>(); + + // load our live data store + store = readStore(getFileName()); + + // create file list of files we should have by now + List versions = new ArrayList<>(); + versions.add(Version.VERSION); + versions.addAll(Version.history); + + // go through the list one by one + versions.forEach(version -> { + String filename = getFileName() + "_" + version; + if (new File(absolutePathOfStorageDir, filename).exists()) { + // if it is there already, load + history.put(version, readStore(getFileName() + "_" + version)); + } else { + // either copy and split + history.put(version, copyAndSplit(version, postFix)); + } + }); + + // load our live data store again - ie to make sure our storage is indeed our live store + storage.initAndGetPersistedWithFileName(getFileName(), 10); + } + + /** + * Bluntly copy and pasted from {@link StoreService} because: + *
  • The member function there is private
  • + *
  • it does not match our interface
  • + *
  • is a temporary solutions, until https://github.com/bisq-network/projects/issues/29
+ * + * @param name + * @return store + */ + private T readStore(String name) { + T store = storage.initAndGetPersistedWithFileName(name, 100); + if (store != null) { + log.info("{}: size of {}: {} MB", this.getClass().getSimpleName(), + storage.getClass().getSimpleName(), + store.toProtoMessage().toByteArray().length / 1_000_000D); + } else { + store = createStore(); + } + + return store; + } + + /** + * Copy the missing data store from resources and remove its object from the live store. + * + * @param version to identify the data store eg. "1.3.4" + * @param postFix the global postfix eg. "_BTC_MAINNET" + * @return the freshly copied and loaded data store + */ + private SplitStore copyAndSplit(String version, String postFix) { + // if not, copy and split + final File destinationFile = new File(absolutePathOfStorageDir, getFileName() + "_" + version); + String resourceFileName = destinationFile.getName() + postFix; // postFix has a preceding "_" already + try { + log.info("We copy resource to file: resourceFileName={}, destinationFile={}", resourceFileName, destinationFile); + FileUtil.resourceToFile(resourceFileName, destinationFile); + } catch (ResourceNotFoundException e) { + log.info("Could not find resourceFile {}. That is expected if none is provided yet.", resourceFileName); + } catch (Throwable e) { + log.error("Could not copy resourceFile {} to {}.\n{}", resourceFileName, destinationFile.getAbsolutePath(), e.getMessage()); + e.printStackTrace(); + } + + // split + // - get all + SplitStore historicalStore = readStore(destinationFile.getName()); + // - subtract all that is in resource files + store.getMap().keySet().removeAll(historicalStore.getMap().keySet()); + + // - create new file with leftovers + storage.initAndGetPersisted(store, 0); + storage.queueUpForSave(); + + return historicalStore; + } +} diff --git a/p2p/src/main/resources/AccountAgeWitnessStore_1.3.5_BTC_MAINNET b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.5_BTC_MAINNET new file mode 100644 index 00000000000..5f30589222b --- /dev/null +++ b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.5_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dc5e4ad24e6c1457d448022a21da47bda3728dc400ad52eeb874889e1b44d8f +size 1914319 diff --git a/p2p/src/main/resources/AccountAgeWitnessStore_1.3.6_BTC_MAINNET b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.6_BTC_MAINNET new file mode 100644 index 00000000000..612d2ee70df --- /dev/null +++ b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.6_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:270254b409fb57bb4acf39cae0670e4e597d2688202d36fe9cdd212c2feb24ed +size 59992 diff --git a/p2p/src/main/resources/AccountAgeWitnessStore_1.3.7_BTC_MAINNET b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.7_BTC_MAINNET new file mode 100644 index 00000000000..7c72ea3dba2 --- /dev/null +++ b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.7_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a82466b1cadc97e3868c99d2832e902469164486b9ae1ec6558961f71c410ec6 +size 58318 diff --git a/p2p/src/main/resources/AccountAgeWitnessStore_1.3.8_BTC_MAINNET b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.8_BTC_MAINNET new file mode 100644 index 00000000000..2cd4bc95078 --- /dev/null +++ b/p2p/src/main/resources/AccountAgeWitnessStore_1.3.8_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e16fedb10f60ca5f4efcd42dfb5433217445e9c77035e539bf089591e739975c +size 88574 diff --git a/p2p/src/main/resources/AccountAgeWitnessStore_BTC_MAINNET b/p2p/src/main/resources/AccountAgeWitnessStore_BTC_MAINNET deleted file mode 100644 index 9fd395ea388..00000000000 --- a/p2p/src/main/resources/AccountAgeWitnessStore_BTC_MAINNET +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54e87ad4ca9e79fff6da1dbdc7f3b1af069e1b43cdcbac388a043bbafd155e64 -size 1825752 diff --git a/p2p/src/main/resources/DaoStateStore_BTC_MAINNET b/p2p/src/main/resources/DaoStateStore_BTC_MAINNET index ab160e8b201..557fc57c2b0 100644 --- a/p2p/src/main/resources/DaoStateStore_BTC_MAINNET +++ b/p2p/src/main/resources/DaoStateStore_BTC_MAINNET @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da8eb98c10af8c9a99a54cac6304fd68d6ff5f90604347f5a5825cf56c4e7533 -size 76161888 +oid sha256:023df03645498b9603bcab4394a4a0361b63a3a39583e4644f16de6625c9390a +size 83140772 diff --git a/p2p/src/main/resources/SignedWitnessStore_1.3.5_BTC_MAINNET b/p2p/src/main/resources/SignedWitnessStore_1.3.5_BTC_MAINNET new file mode 100644 index 00000000000..2f22f569462 --- /dev/null +++ b/p2p/src/main/resources/SignedWitnessStore_1.3.5_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a089f2076a11caa1f3368c5bfc373e0b85ea5e7c828d2ceee7aab39c2a44b5 +size 3630451 diff --git a/p2p/src/main/resources/SignedWitnessStore_1.3.6_BTC_MAINNET b/p2p/src/main/resources/SignedWitnessStore_1.3.6_BTC_MAINNET new file mode 100644 index 00000000000..56b36618c7a --- /dev/null +++ b/p2p/src/main/resources/SignedWitnessStore_1.3.6_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a457c277e9bf72d8aa9a0792d6843271ebd030a87b8e38fccee8b4582e2836b +size 189112 diff --git a/p2p/src/main/resources/SignedWitnessStore_1.3.7_BTC_MAINNET b/p2p/src/main/resources/SignedWitnessStore_1.3.7_BTC_MAINNET new file mode 100644 index 00000000000..7d547a47719 --- /dev/null +++ b/p2p/src/main/resources/SignedWitnessStore_1.3.7_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf0749ee2edb07c9d647621656c621043090c06ae780e23d556b53033e85073e +size 581520 diff --git a/p2p/src/main/resources/SignedWitnessStore_1.3.8_BTC_MAINNET b/p2p/src/main/resources/SignedWitnessStore_1.3.8_BTC_MAINNET new file mode 100644 index 00000000000..4fdc946522f --- /dev/null +++ b/p2p/src/main/resources/SignedWitnessStore_1.3.8_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4027b836bfcc4178fe131e8c5c08d10821cd1aa33cb57bab5fac9a16a3c25745 +size 438640 diff --git a/p2p/src/main/resources/SignedWitnessStore_BTC_MAINNET b/p2p/src/main/resources/SignedWitnessStore_BTC_MAINNET deleted file mode 100644 index 6760f74d3ef..00000000000 --- a/p2p/src/main/resources/SignedWitnessStore_BTC_MAINNET +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c951d95fb1d9fc6ba7e0d26bc94b12533c44ad12782705779df00d617b00d1f -size 3191819 diff --git a/p2p/src/main/resources/TradeStatistics2Store_1.3.5_BTC_MAINNET b/p2p/src/main/resources/TradeStatistics2Store_1.3.5_BTC_MAINNET new file mode 100644 index 00000000000..3e5b730c838 --- /dev/null +++ b/p2p/src/main/resources/TradeStatistics2Store_1.3.5_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49dac2909810c42144f4a62979ecd588659a9e7a874bb1a6c4867a6602e3b3eb +size 15862823 diff --git a/p2p/src/main/resources/TradeStatistics2Store_1.3.6_BTC_MAINNET b/p2p/src/main/resources/TradeStatistics2Store_1.3.6_BTC_MAINNET new file mode 100644 index 00000000000..696f4922db2 --- /dev/null +++ b/p2p/src/main/resources/TradeStatistics2Store_1.3.6_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4541868eb2a03a84151ca19fe67e4b3b22ab89cefa0342603d70f0d89471abe +size 595840 diff --git a/p2p/src/main/resources/TradeStatistics2Store_1.3.7_BTC_MAINNET b/p2p/src/main/resources/TradeStatistics2Store_1.3.7_BTC_MAINNET new file mode 100644 index 00000000000..e93bce51792 --- /dev/null +++ b/p2p/src/main/resources/TradeStatistics2Store_1.3.7_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:487dbff48cb101fea5db0e54f556c874d345ddf56c2eded4e2d85c59db0d35a2 +size 598071 diff --git a/p2p/src/main/resources/TradeStatistics2Store_1.3.8_BTC_MAINNET b/p2p/src/main/resources/TradeStatistics2Store_1.3.8_BTC_MAINNET new file mode 100644 index 00000000000..b1b49939733 --- /dev/null +++ b/p2p/src/main/resources/TradeStatistics2Store_1.3.8_BTC_MAINNET @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21b93f17351defea68332d9bce9d53e8a211b08cf7093f6e40e622c18ae8c36e +size 881553 diff --git a/p2p/src/main/resources/TradeStatistics2Store_BTC_MAINNET b/p2p/src/main/resources/TradeStatistics2Store_BTC_MAINNET deleted file mode 100644 index 7207f792e35..00000000000 --- a/p2p/src/main/resources/TradeStatistics2Store_BTC_MAINNET +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5688976d2b682c32e7404138312cc86472050e6c1c77fdfa92d99410efea3468 -size 14981277 diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRequestDataTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRequestDataTest.java index 27e522daaad..7fedd330c23 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRequestDataTest.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRequestDataTest.java @@ -98,7 +98,8 @@ public void buildPreliminaryGetDataRequest_EmptyP2PDataStore() { Assert.assertEquals(getDataRequest.getNonce(), 1); Assert.assertEquals(getDataRequest.getSupportedCapabilities(), Capabilities.app); - Assert.assertTrue(getDataRequest.getExcludedKeys().isEmpty()); + Assert.assertEquals(1, getDataRequest.getExcludedKeys().size()); + Assert.assertTrue(getDataRequest.getExcludedKeys().stream().map(bytes -> new String(bytes).trim()).filter(s -> s.matches("^[0-9]\\.[0-9]\\.[0-9]$")).findFirst().isPresent()); } // TESTCASE: P2PDataStorage with no entries returns an empty PreliminaryGetDataRequest @@ -109,7 +110,8 @@ public void buildGetUpdatedDataRequest_EmptyP2PDataStore() { Assert.assertEquals(getDataRequest.getNonce(), 1); Assert.assertEquals(getDataRequest.getSenderNodeAddress(), this.localNodeAddress); - Assert.assertTrue(getDataRequest.getExcludedKeys().isEmpty()); + Assert.assertEquals(1, getDataRequest.getExcludedKeys().size()); + Assert.assertTrue(getDataRequest.getExcludedKeys().stream().map(bytes -> new String(bytes).trim()).filter(s -> s.matches("^[0-9]\\.[0-9]\\.[0-9]$")).findFirst().isPresent()); } // TESTCASE: P2PDataStorage with PersistableNetworkPayloads and ProtectedStorageEntry generates @@ -131,7 +133,8 @@ public void buildPreliminaryGetDataRequest_FilledP2PDataStore() throws NoSuchAlg Assert.assertEquals(getDataRequest.getNonce(), 1); Assert.assertEquals(getDataRequest.getSupportedCapabilities(), Capabilities.app); - Assert.assertEquals(4, getDataRequest.getExcludedKeys().size()); + Assert.assertEquals(5, getDataRequest.getExcludedKeys().size()); + Assert.assertTrue(getDataRequest.getExcludedKeys().stream().map(bytes -> new String(bytes).trim()).filter(s -> s.matches("^[0-9]\\.[0-9]\\.[0-9]$")).findFirst().isPresent()); Assert.assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd1.getHash())); Assert.assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd2.getHash())); Assert.assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), @@ -160,7 +163,8 @@ public void requestData_FilledP2PDataStore_GetUpdatedDataRequest() throws NoSuch Assert.assertEquals(getDataRequest.getNonce(), 1); Assert.assertEquals(getDataRequest.getSenderNodeAddress(), this.localNodeAddress); - Assert.assertEquals(4, getDataRequest.getExcludedKeys().size()); + Assert.assertEquals(5, getDataRequest.getExcludedKeys().size()); + Assert.assertTrue(getDataRequest.getExcludedKeys().stream().map(bytes -> new String(bytes).trim()).filter(s -> s.matches("^[0-9]\\.[0-9]\\.[0-9]$")).findFirst().isPresent()); Assert.assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd1.getHash())); Assert.assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), toAdd2.getHash())); Assert.assertTrue(byteSetContains(getDataRequest.getExcludedKeys(), diff --git a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java index e71246a0310..04aba199e03 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java @@ -83,7 +83,7 @@ public class TestState { this.mockBroadcaster, new AppendOnlyDataStoreServiceFake(), this.protectedDataStoreService, mock(ResourceDataStoreService.class), - this.mockSeqNrStorage, this.clockFake, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); + this.mockSeqNrStorage, null, this.clockFake, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); this.appendOnlyDataStoreListener = mock(AppendOnlyDataStoreListener.class); this.hashMapChangedListener = mock(HashMapChangedListener.class); @@ -129,7 +129,7 @@ private static P2PDataStorage createP2PDataStorageForTest( broadcaster, new AppendOnlyDataStoreServiceFake(), protectedDataStoreService, mock(ResourceDataStoreService.class), - sequenceNrMapStorage, clock, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); + sequenceNrMapStorage, null, clock, MAX_SEQUENCE_NUMBER_MAP_SIZE_BEFORE_PURGE); // Currently TestState only supports reading ProtectedStorageEntries off disk. p2PDataStorage.readFromResources("unused"); diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java index 155e4708be7..4a693df3772 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java @@ -37,6 +37,10 @@ public AppendOnlyDataStoreServiceFake() { map = new HashMap<>(); } + public Map getMap(String filter) { + return getMap(); + } + public Map getMap() { return map; } diff --git a/reduce_initial_request_size_test.sh b/reduce_initial_request_size_test.sh new file mode 100755 index 00000000000..86e0580daf5 --- /dev/null +++ b/reduce_initial_request_size_test.sh @@ -0,0 +1,165 @@ +#!/bin/sh + +LAST_RELEASE='1.3.5' +PR='reduce_initial_request_size' + +install() +{ + mkdir -p "installdir/$2" + cp "bisq-$1" "installdir/$2/" + cp -r lib "installdir/$2/" +} + +declare -A cmd +cmd['bitcoind']="bitcoind -regtest -prune=0 -txindex=1 -peerbloomfilters=1 -server -rpcuser=bisqdao -rpcpassword=bsq -datadir=.localnet/bitcoind -blocknotify='.localnet/bitcoind/blocknotify %s'" +cmd['alice']="installdir/alice/bisq-desktop --baseCurrencyNetwork=BTC_REGTEST --useLocalhostForP2P=true --useDevPrivilegeKeys=true --nodePort=5555 --fullDaoNode=true --rpcUser=bisqdao --rpcPassword=bsq --rpcBlockNotificationPort=5122 --genesisBlockHeight=111 --genesisTxId=30af0050040befd8af25068cc697e418e09c2d8ebd8d411d2240591b9ec203cf --appDataDir=.localnet/alice --appName=Alice" +cmd['bob']="installdir/bob/bisq-desktop --baseCurrencyNetwork=BTC_REGTEST --useLocalhostForP2P=true --useDevPrivilegeKeys=true --nodePort=6666 --appDataDir=.localnet/bob --appName=Bob" +cmd['mediator']="installdir/mediator/bisq-desktop --baseCurrencyNetwork=BTC_REGTEST --useLocalhostForP2P=true --useDevPrivilegeKeys=true --nodePort=4444 --appDataDir=.localnet/mediator --appName=Mediator" +cmd['seednode']="installdir/seednode/bisq-seednode --baseCurrencyNetwork=BTC_REGTEST --useLocalhostForP2P=true --useDevPrivilegeKeys=true --fullDaoNode=true --rpcUser=bisqdao --rpcPassword=bsq --rpcBlockNotificationPort=5120 --nodePort=2002 --userDataDir=.localnet --appName=seednode" +cmd['seednode2']="installdir/seednode2/bisq-seednode --baseCurrencyNetwork=BTC_REGTEST --useLocalhostForP2P=true --useDevPrivilegeKeys=true --fullDaoNode=true --rpcUser=bisqdao --rpcPassword=bsq --rpcBlockNotificationPort=5121 --nodePort=3002 --userDataDir=.localnet --appName=seednode2" + +run() +{ + # create a new screen session named 'localnet' + screen -dmS localnet + # deploy each node in its own named screen window + for target in \ + bitcoind \ + seednode \ + seednode2 \ + alice \ + bob \ + mediator; do \ +# echo "$target: ${cmd[$target]}" + screen -S localnet -X screen -t $target; \ + screen -S localnet -p $target -X stuff "${cmd[$target]}\n"; \ + sleep 3 + done; + # give bitcoind rpc server time to start + sleep 5 + # generate a block to ensure Bisq nodes get dao-synced + make block +} + +# - shut down anything again +staap() +{ + # kill all Bitcoind and Bisq nodes running in screen windows + screen -S localnet -X at "#" stuff "^C" + # quit all screen windows which results in killing the session + screen -S localnet -X at "#" kill + set +e + screen -wipe + set -e +} + +check() +{ + # gather data + for target in \ + seednode \ + seednode2 \ + alice \ + bob \ + mediator; do \ + echo "$target:" >> result.log; \ + grep -Eo ": (Sending .*Request|Received .*Response) with [0-9\.]* kB" .localnet/$target/bisq.log >> result.log; \ + rm .localnet/$target/bisq.log; \ + done; +} + +# clean everything for a fresh test run +rm -rf .localnet +rm -rf installdir +staap + +set -e + +# deploy configuration files and start bitcoind +make localnet +for target in \ + seednode \ + seednode2 \ + alice \ + bob \ + mediator; do \ + mkdir -p .localnet/$target/btc_regtest/db/; \ + cp PreferencesPayload .localnet/$target/btc_regtest/db/; \ +done; + + +# - borrow mainnet data stores to better illustrate stuff +cd p2p/src/main/resources/ +cp TradeStatistics2Store_BTC_MAINNET TradeStatistics2Store_BTC_REGTEST +cp AccountAgeWitnessStore_BTC_MAINNET AccountAgeWitnessStore_BTC_REGTEST +cp SignedWitnessStore_BTC_MAINNET SignedWitnessStore_BTC_REGTEST +cd - + +# start with release setup +# - get sources for release +git checkout release/v$LAST_RELEASE + +# - build initial binaries and file structure +./gradlew clean +./gradlew :seednode:build +./gradlew :desktop:build + +# - install binaries +install seednode seednode +install seednode seednode2 +install desktop alice +install desktop bob +install desktop mediator + +# - fire up all of it +run + +# - setup mediator/refund agent +sleep 5 +read -p "Wrap up first test case?" + +# - shut down everything +staap + +echo "##### Sanity check ###########################################" > result.log +check + +# upgrade to PR +git checkout $PR + +# create release data stores +cd p2p/src/main/resources/ +cp TradeStatistics2Store_BTC_REGTEST TradeStatistics2Store_${LAST_RELEASE}_BTC_REGTEST +cp AccountAgeWitnessStore_BTC_REGTEST AccountAgeWitnessStore_${LAST_RELEASE}_BTC_REGTEST +cp SignedWitnessStore_BTC_REGTEST SignedWitnessStore_${LAST_RELEASE}_BTC_REGTEST +cd - +./gradlew :seednode:build + +# install seednode binaries +install seednode seednode + +# fire up all of it +run + +sleep 5 +read -p "Wrap up second test case?" + +# shut down anything again +staap + +echo "##### After upgrading one seednode ###########################" >> result.log +check + +## install client binaries +install desktop alice + +## fire up all of it +run + +read -p "Wrap up third test case?" + +# shut down anything again +staap + +echo "##### After upgrading one client ###########################" >> result.log +check diff --git a/relay/src/main/resources/version.txt b/relay/src/main/resources/version.txt index 3336003dccd..e05cb3329c6 100644 --- a/relay/src/main/resources/version.txt +++ b/relay/src/main/resources/version.txt @@ -1 +1 @@ -1.3.7 +1.3.8 diff --git a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java index 3db42fa5858..ffa6ac667ed 100644 --- a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java @@ -33,7 +33,7 @@ @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { - private static final String VERSION = "1.3.7"; + private static final String VERSION = "1.3.8"; private SeedNode seedNode; public SeedNodeMain() {